Refactor ExchangePage into Swap and Send pages (#774)

* Part 1, separate swap and send, move duplicate code into separate files

* Move some more constants out of the swap/send

* Support defaults from URL parameters

* Implement query string parsing via a redux action

* Finish merging the changes

* Fix the slippage warnings

* Clean up some other files

* More refactoring

* Move the price bar out of the component

* Move advanced details and some computations into utilities

* Make same improvements to send

* Approval hook

* Swap page functional with swap callback hook

* Swap/send page functional with swap callback hook

* Fix lint

* Move styleds.ts

* Fix integration test

* Fix error state in swap and move some things around

* Move send callback out of send page

* Make send behave more like current behavior

* Differentiate swap w/o send on send page from swap w/o send on swap page

* Remove userAdvanced which was always false

* Remove the price bar which is not used in the new UI

* Delete the file

* Fix null in the send dialog and move another component out of send

* Move the swap modal header out to another file

* Include change after merge

* Add recipient to swap message

* Keep input token selected if user has no balance and clicks send with swap

* Move the modal footer out of send component

* Remove the hard coded estimated time

* Fix the label/action for swap/sends

* Share the swap modal footer between swap and send

* Fix integration test

* remove margin from popper to suppress warnings

fix missing ENS name recipient link

default deadline to 15 minutes

* ensure useApproveCallback returns accurate data

* clean up callbacks

* extra space

* Re-apply ignored changes from v2 branch ExchangePage file

Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
This commit is contained in:
Moody Salem 2020-05-16 17:55:22 -04:00 committed by GitHub
parent 51e929bd1e
commit 095beae0c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 2168 additions and 2164 deletions

@ -2,8 +2,8 @@ import { TEST_ADDRESS_NEVER_USE } from '../support/commands'
describe('Landing Page', () => {
beforeEach(() => cy.visit('/'))
it('loads exchange page', () => {
cy.get('#exchange-page')
it('loads swap page', () => {
cy.get('#swap-page')
})
it('redirects to url /swap', () => {

@ -35,8 +35,8 @@ describe('Swap', () => {
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click()
cy.get('#swap-currency-input .token-amount-input').type('0.001')
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
cy.get('#exchange-show-advanced').click()
cy.get('#exchange-swap-button').click()
cy.get('#exchange-page-confirm-swap-or-send').should('contain', 'Confirm Swap')
cy.get('#show-advanced').click()
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
})
})

@ -37,6 +37,7 @@
"jazzicon": "^1.5.0",
"polished": "^3.3.2",
"qrcode.react": "^0.9.3",
"qs": "^6.9.4",
"react": "^16.13.1",
"react-device-detect": "^1.6.2",
"react-dom": "^16.13.1",
@ -60,6 +61,7 @@
"@ethersproject/wallet": "^5.0.0-beta.141",
"@types/jest": "^25.2.1",
"@types/node": "^13.13.5",
"@types/qs": "^6.9.2",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.7",
"@types/react-redux": "^7.1.8",

@ -1,181 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react'
import styled from 'styled-components'
import QuestionHelper from '../Question'
import NumericalInput from '../NumericalInput'
import { Link } from '../../theme/components'
import { TYPE } from '../../theme'
import { AutoColumn } from '../Column'
import { ButtonRadio } from '../Button'
import { useTranslation } from 'react-i18next'
import Row, { RowBetween, RowFixed } from '../Row'
const InputWrapper = styled(RowBetween)<{ active?: boolean; error?: boolean }>`
width: 200px;
background-color: ${({ theme }) => theme.bg1};
border-radius: 8px;
padding: 4px 8px;
border: 1px solid transparent;
border: ${({ active, error, theme }) =>
error ? '1px solid ' + theme.red1 : active ? '1px solid ' + theme.primary1 : ''};
`
const SLIPPAGE_INDEX = {
1: 1,
2: 2,
3: 3,
4: 4
}
interface AdvancedSettingsProps {
setIsOpen: (boolean) => void
setDeadline: (number) => void
allowedSlippage: number
setAllowedSlippage: (number) => void
}
export default function AdvancedSettings({
setIsOpen,
setDeadline,
allowedSlippage,
setAllowedSlippage
}: AdvancedSettingsProps) {
// text translation
const { t } = useTranslation()
const [deadlineInput, setDeadlineInput] = useState(20)
const [slippageInput, setSlippageInput] = useState<string>('')
const [activeIndex, setActiveIndex] = useState(SLIPPAGE_INDEX[2])
const [slippageInputError, setSlippageInputError] = useState(null) // error
const parseCustomInput = useCallback(
val => {
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
if (val > 5) {
setSlippageInputError('Your transaction may be front-run.')
} else {
setSlippageInputError(null)
}
if (acceptableValues.some(a => a.test(val))) {
setSlippageInput(val)
setAllowedSlippage(val * 100)
}
},
[setAllowedSlippage]
)
function parseCustomDeadline(val) {
const acceptableValues = [/^$/, /^\d+$/]
if (acceptableValues.some(re => re.test(val))) {
setDeadlineInput(val)
setDeadline(val * 60)
}
}
// update settings based on current slippage selected
useEffect(() => {
if (allowedSlippage === 10) {
setActiveIndex(1)
} else if (allowedSlippage === 50) {
setActiveIndex(2)
} else if (allowedSlippage === 100) {
setActiveIndex(3)
} else {
setActiveIndex(4)
setSlippageInput('' + allowedSlippage / 100)
parseCustomInput(allowedSlippage)
}
}, [allowedSlippage, parseCustomInput])
return (
<AutoColumn gap="lg">
<Link
onClick={() => {
setIsOpen(false)
}}
>
back
</Link>
<RowBetween>
<TYPE.main>Front-running tolerance</TYPE.main>
<QuestionHelper text={t('toleranceExplanation')} />
</RowBetween>
<Row>
<ButtonRadio
active={SLIPPAGE_INDEX[1] === activeIndex}
padding="4px 6px"
style={{ marginRight: '16px' }}
width={'60px'}
onClick={() => {
setActiveIndex(SLIPPAGE_INDEX[1])
setAllowedSlippage(10)
}}
>
0.1%
</ButtonRadio>
<ButtonRadio
active={SLIPPAGE_INDEX[2] === activeIndex}
padding="4px 6px"
style={{ marginRight: '16px' }}
width={'180px'}
onClick={() => {
setActiveIndex(SLIPPAGE_INDEX[2])
setAllowedSlippage(50)
}}
>
0.5% (suggested)
</ButtonRadio>
<ButtonRadio
active={SLIPPAGE_INDEX[3] === activeIndex}
padding="4px"
width={'60px'}
onClick={() => {
setActiveIndex(SLIPPAGE_INDEX[3])
setAllowedSlippage(100)
}}
>
1%
</ButtonRadio>
</Row>
<RowFixed>
<InputWrapper active={SLIPPAGE_INDEX[4] === activeIndex} error={slippageInputError}>
<NumericalInput
align={slippageInput ? 'right' : 'left'}
value={slippageInput || ''}
onUserInput={val => {
parseCustomInput(val)
setActiveIndex(SLIPPAGE_INDEX[4])
}}
placeholder="Custom"
onClick={() => {
setActiveIndex(SLIPPAGE_INDEX[4])
if (slippageInput) {
parseCustomInput(slippageInput)
}
}}
/>
%
</InputWrapper>
{slippageInputError && (
<TYPE.error error={true} fontSize={12} style={{ marginLeft: '10px' }}>
Your transaction may be front-run
</TYPE.error>
)}
</RowFixed>
<RowBetween>
<TYPE.main>Adjust deadline (minutes from now)</TYPE.main>
</RowBetween>
<RowFixed>
<InputWrapper>
<NumericalInput
value={deadlineInput}
onUserInput={val => {
parseCustomDeadline(val)
}}
/>
</InputWrapper>
</RowFixed>
</AutoColumn>
)
}

@ -1,220 +0,0 @@
import React, { useState } from 'react'
import TokenLogo from '../TokenLogo'
import { TYPE } from '../../theme'
import { Text } from 'rebass'
import { Hover } from '../../theme'
import { GreyCard } from '../Card'
import { AutoColumn } from '../Column'
import { RowBetween, AutoRow } from '../Row'
import { Copy as CopyIcon, BarChart2, Info, Share, ChevronDown, ChevronUp, Plus } from 'react-feather'
import DoubleLogo from '../DoubleLogo'
import { ButtonSecondary, ButtonGray } from '../Button'
import { Token } from '@uniswap/sdk'
interface BalanceCardProps {
token0: Token
balance0: boolean
import0: boolean
token1: Token
balance1: boolean
import1: boolean
}
export default function BalanceCard({ token0, balance0, import0, token1, balance1, import1 }: BalanceCardProps) {
const [showInfo, setshowInfo] = useState(false)
return (
<AutoRow
gap="lg"
justify={'space-between'}
style={{
minWidth: '200px',
maxWidth: '355px',
flexWrap: 'nowrap',
alignItems: 'flex-end',
zIndex: 99
}}
>
<AutoColumn style={{ width: '100%', padding: '12px' }}>
{!showInfo ? (
<Hover>
<GreyCard padding="16px 20px">
<RowBetween onClick={() => setshowInfo(true)} padding={' 0'}>
<Text fontSize={16} fontWeight={500} style={{ userSelect: 'none' }}>
Show selection details
</Text>
<ChevronDown color={'#565A69'} />
</RowBetween>
</GreyCard>
</Hover>
) : (
<Hover>
<GreyCard padding="px 20px" style={{ marginTop: '0' }}>
<RowBetween onClick={() => setshowInfo(false)} padding={'0px'}>
<Text fontSize={16} color="#565A69" fontWeight={500} style={{ userSelect: 'none' }}>
Hide selection details
</Text>
<ChevronUp color="#565A69" />
</RowBetween>
</GreyCard>
</Hover>
)}
{showInfo && (
<AutoColumn gap="md" style={{ marginTop: '1rem' }}>
{token0 && balance0 && (
// <Hover onClick={() => setDetails0(!details0)}>
<>
<GreyCard padding={'1rem'}>
<RowBetween>
<TYPE.body fontWeight={500}>
{token0?.name} ({token0?.symbol})
</TYPE.body>
<TokenLogo size={'20px'} address={token0?.address} />
</RowBetween>
{import0 && <TYPE.yellow style={{ paddingLeft: '0' }}>Token imported by user</TYPE.yellow>}
<AutoRow gap="sm" justify="flex-start" style={{ marginTop: '1rem' }}>
<ButtonGray padding={'2px'} width={'auto'} style={{ margin: '2px' }}>
<AutoRow gap="sm" justify="space-between" padding={'0 4px'}>
<Info size={14} />
<Text fontWeight={500} fontSize={14} style={{ marginLeft: '6px' }}>
Info
</Text>
</AutoRow>
</ButtonGray>
<ButtonGray padding={'2px'} width={'auto'} style={{ margin: '2px' }}>
<AutoRow gap="sm" justify="space-between" padding={'0 4px'}>
<BarChart2 size={14} />
<Text fontWeight={500} fontSize={14} style={{ marginLeft: '6px' }}>
Charts
</Text>
</AutoRow>
</ButtonGray>
<ButtonGray padding={'2px'} width={'auto'} style={{ margin: '2px' }}>
<AutoRow gap="sm" justify="space-between" padding={'0 4px'}>
<CopyIcon size={14} />
<Text fontWeight={500} fontSize={14} style={{ marginLeft: '6px' }}>
Copy Address
</Text>
</AutoRow>
</ButtonGray>
</AutoRow>
</GreyCard>
</>
)}
{token1 && balance1 && (
// <Hover onClick={() => setDetails1(!details1)}>
<>
<GreyCard padding={'1rem'}>
<RowBetween>
<TYPE.body fontWeight={500}>
{token1?.name} ({token1?.symbol})
</TYPE.body>
<TokenLogo size={'20px'} address={token1?.address} />
</RowBetween>
{import1 && <TYPE.yellow style={{ paddingLeft: '0' }}>Token imported by user</TYPE.yellow>}
<AutoRow gap="sm" justify="flex-start" style={{ marginTop: '1rem' }}>
<ButtonGray padding={'2px'} width={'auto'} style={{ margin: '2px' }}>
<AutoRow gap="sm" justify="space-between" padding={'0 4px'}>
<Info size={14} />
<Text fontWeight={500} fontSize={14} style={{ marginLeft: '6px' }}>
Info
</Text>
</AutoRow>
</ButtonGray>
<ButtonGray padding={'2px'} width={'auto'} style={{ margin: '2px' }}>
<AutoRow gap="sm" justify="space-between" padding={'0 4px'}>
<BarChart2 size={14} />
<Text fontWeight={500} fontSize={14} style={{ marginLeft: '6px' }}>
Charts
</Text>
</AutoRow>
</ButtonGray>
<ButtonGray padding={'2px'} width={'auto'} style={{ margin: '2px' }}>
<AutoRow gap="sm" justify="space-between" padding={'0 4px'}>
<CopyIcon size={14} />
<Text fontWeight={500} fontSize={14} style={{ marginLeft: '6px' }}>
Copy Address
</Text>
</AutoRow>
</ButtonGray>
</AutoRow>
</GreyCard>
</>
)}
<GreyCard padding={'1rem'}>
<RowBetween>
<TYPE.body fontWeight={500}>
{token0?.symbol}:{token1?.symbol}
</TYPE.body>
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
</RowBetween>
{import1 && <TYPE.yellow style={{ paddingLeft: '32px' }}>Token imported by user</TYPE.yellow>}
<AutoRow gap="sm" justify="flex-start" style={{ marginTop: '1rem' }}>
<ButtonGray padding={'2px'} width={'auto'} style={{ margin: '2px' }}>
<AutoRow gap="sm" justify="space-between" padding={'0 4px'}>
<Info size={14} />
<Text fontWeight={500} fontSize={14} style={{ marginLeft: '6px' }}>
Info
</Text>
</AutoRow>
</ButtonGray>
<ButtonGray padding={'2px'} width={'auto'} style={{ margin: '2px' }}>
<AutoRow gap="sm" justify="space-between" padding={'0 4px'}>
<BarChart2 size={14} />
<Text fontWeight={500} fontSize={14} style={{ marginLeft: '6px' }}>
Charts
</Text>
</AutoRow>
</ButtonGray>
<ButtonGray padding={'2px'} width={'auto'} style={{ margin: '2px' }}>
<AutoRow gap="sm" justify="space-between" padding={'0 4px'}>
<CopyIcon size={14} />
<Text fontWeight={500} fontSize={14} style={{ marginLeft: '6px' }}>
Copy Address
</Text>
</AutoRow>
</ButtonGray>
<ButtonGray padding={'2px'} width={'auto'} style={{ margin: '2px' }}>
<AutoRow gap="sm" justify="space-between" padding={'0 4px'}>
<Plus size={14} />
<Text fontWeight={500} fontSize={14} style={{ marginLeft: '6px' }}>
Add Liquidity
</Text>
</AutoRow>
</ButtonGray>
</AutoRow>
</GreyCard>
</AutoColumn>
)}
</AutoColumn>
<AutoRow
style={{
position: 'fixed',
bottom: '16px',
right: '132px',
width: 'fit-content'
}}
>
{token1 && (
<ButtonSecondary
style={{
padding: ' 8px',
marginLeft: '8px',
width: 'fit-content'
}}
>
<Share size={16} style={{ marginRight: '8px' }} />
Share
</ButtonSecondary>
)}
</AutoRow>
</AutoRow>
)
}

@ -118,11 +118,12 @@ function ConfirmationModal({
</ButtonPrimary>
</>
)}
<Text fontSize={12} color="#565A69" textAlign="center">
{pendingConfirmation
? 'Confirm this transaction in your wallet'
: `Estimated time until confirmation: 3 min`}
</Text>
{pendingConfirmation && (
<Text fontSize={12} color="#565A69" textAlign="center">
Confirm this transaction in your wallet
</Text>
)}
</AutoColumn>
</Section>
</Wrapper>

@ -27,7 +27,7 @@ const STEP = {
SHOW_CREATE_PAGE: 'SHOW_CREATE_PAGE' // show create page
}
function CreatePool({ history }: RouteComponentProps<{}>) {
function CreatePool({ history }: RouteComponentProps) {
const { chainId } = useWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)

@ -3,13 +3,14 @@ import React, { useState, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import '@reach/tooltip/styles.css'
import { darken } from 'polished'
import { Field } from '../../state/swap/actions'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo'
import SearchModal from '../SearchModal'
import { RowBetween } from '../Row'
import { TYPE, Hover } from '../../theme'
import { TYPE, CursorPointer } from '../../theme'
import { Input as NumericalInput } from '../NumericalInput'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
@ -120,10 +121,10 @@ const StyledBalanceMax = styled.button`
interface CurrencyInputPanelProps {
value: string
field: number
onUserInput: (field: number, val: string) => void
onMax: () => void
atMax: boolean
field: string
onUserInput: (field: string, val: string) => void
onMax?: () => void
showMaxButton: boolean
label?: string
urlAddedTokens?: Token[]
onTokenSelection?: (tokenAddress: string) => void
@ -135,7 +136,6 @@ interface CurrencyInputPanelProps {
hideInput?: boolean
showSendWithSwap?: boolean
otherSelectedTokenAddress?: string | null
advanced?: boolean
id: string
}
@ -144,7 +144,7 @@ export default function CurrencyInputPanel({
field,
onUserInput,
onMax,
atMax,
showMaxButton,
label = 'Input',
urlAddedTokens = [], // used
onTokenSelection = null,
@ -156,7 +156,6 @@ export default function CurrencyInputPanel({
hideInput = false,
showSendWithSwap = false,
otherSelectedTokenAddress = null,
advanced = false,
id
}: CurrencyInputPanelProps) {
const { t } = useTranslation()
@ -176,7 +175,7 @@ export default function CurrencyInputPanel({
{label}
</TYPE.body>
{account && (
<Hover>
<CursorPointer>
<TYPE.body
onClick={onMax}
color={theme.text2}
@ -188,7 +187,7 @@ export default function CurrencyInputPanel({
? 'Balance: ' + userTokenBalance?.toSignificant(6)
: ' -'}
</TYPE.body>
</Hover>
</CursorPointer>
)}
</RowBetween>
</LabelRow>
@ -203,7 +202,7 @@ export default function CurrencyInputPanel({
onUserInput(field, val)
}}
/>
{account && !advanced && !!token?.address && !atMax && label !== 'To' && (
{account && !!token?.address && showMaxButton && label !== 'To' && (
<StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>
)}
</>
@ -249,7 +248,7 @@ export default function CurrencyInputPanel({
showSendWithSwap={showSendWithSwap}
hiddenToken={token?.address}
otherSelectedTokenAddress={otherSelectedTokenAddress}
otherSelectedText={field === 0 ? 'Selected as output' : 'Selected as input'}
otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'}
/>
)}
</InputPanel>

File diff suppressed because it is too large Load Diff

@ -1,136 +0,0 @@
import { WETH } from '@uniswap/sdk'
import { useReducer } from 'react'
import { useWeb3React } from '../../hooks'
import { QueryParams } from '../../utils'
export enum Field {
INPUT,
OUTPUT
}
export interface SwapState {
independentField: Field
typedValue: string
[Field.INPUT]: {
address: string | undefined
}
[Field.OUTPUT]: {
address: string | undefined
}
}
export function initializeSwapState({
inputTokenAddress,
outputTokenAddress,
typedValue,
independentField
}): SwapState {
return {
independentField: independentField,
typedValue: typedValue,
[Field.INPUT]: {
address: inputTokenAddress
},
[Field.OUTPUT]: {
address: outputTokenAddress
}
}
}
export enum SwapAction {
SELECT_TOKEN,
SWITCH_TOKENS,
TYPE
}
export interface Payload {
[SwapAction.SELECT_TOKEN]: {
field: Field
address: string
}
[SwapAction.SWITCH_TOKENS]: undefined
[SwapAction.TYPE]: {
field: Field
typedValue: string
}
}
export function reducer(
state: SwapState,
action: {
type: SwapAction
payload: Payload[SwapAction]
}
): SwapState {
switch (action.type) {
case SwapAction.SELECT_TOKEN: {
const { field, address } = action.payload as Payload[SwapAction.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 SwapAction.SWITCH_TOKENS: {
return {
...state,
independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT,
[Field.INPUT]: { address: state[Field.OUTPUT].address },
[Field.OUTPUT]: { address: state[Field.INPUT].address }
}
}
case SwapAction.TYPE: {
const { field, typedValue } = action.payload as Payload[SwapAction.TYPE]
return {
...state,
independentField: field,
typedValue
}
}
default: {
throw Error
}
}
}
export function useSwapStateReducer(params: QueryParams) {
const { chainId } = useWeb3React()
return useReducer(
reducer,
{
independentField: params.outputTokenAddress && !params.inputTokenAddress ? Field.OUTPUT : Field.INPUT,
inputTokenAddress: params.inputTokenAddress ? params.inputTokenAddress : WETH[chainId].address,
outputTokenAddress: params.outputTokenAddress ? params.outputTokenAddress : '',
typedValue:
params.inputTokenAddress && !params.outputTokenAddress
? params.inputTokenAmount
? params.inputTokenAmount
: ''
: !params.inputTokenAddress && params.outputTokenAddress
? params.outputTokenAmount
? params.outputTokenAmount
: ''
: params.inputTokenAddress && params.outputTokenAddress
? params.inputTokenAmount
? params.inputTokenAmount
: ''
: ''
? ''
: ''
? ''
: ''
},
initializeSwapState
)
}

@ -4,7 +4,7 @@ import { darken } from 'polished'
import { useTranslation } from 'react-i18next'
import { withRouter, NavLink, Link as HistoryLink, RouteComponentProps } from 'react-router-dom'
import { Hover } from '../../theme'
import { CursorPointer } from '../../theme'
import { ArrowLeft } from 'react-feather'
import { RowBetween } from '../Row'
import QuestionHelper from '../Question'
@ -105,9 +105,9 @@ function NavigationTabs({ location: { pathname }, history }: RouteComponentProps
{adding || removing ? (
<Tabs>
<RowBetween style={{ padding: '1rem' }}>
<Hover onClick={() => history.push('/pool')}>
<CursorPointer onClick={() => history.push('/pool')}>
<ArrowLink />
</Hover>
</CursorPointer>
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
<QuestionHelper
text={

@ -40,7 +40,6 @@ const fadeIn = keyframes`
const Popup = styled.div`
width: 228px;
z-index: 9999;
margin: 0.4rem;
padding: 0.6rem 1rem;
line-height: 150%;
background: ${({ theme }) => theme.bg1};

@ -15,7 +15,7 @@ import TokenLogo from '../TokenLogo'
import DoubleTokenLogo from '../DoubleLogo'
import Column, { AutoColumn } from '../Column'
import { Text } from 'rebass'
import { Hover } from '../../theme'
import { CursorPointer } from '../../theme'
import { ArrowLeft } from 'react-feather'
import { CloseIcon } from '../../theme/components'
import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
@ -557,13 +557,13 @@ function SearchModal({
<PaddedColumn gap="lg">
<RowBetween>
<RowFixed>
<Hover>
<CursorPointer>
<ArrowLeft
onClick={() => {
setShowTokenImport(false)
}}
/>
</Hover>
</CursorPointer>
<Text fontWeight={500} fontSize={16} marginLeft={'10px'}>
Import A Token
</Text>

@ -123,19 +123,14 @@ const Percent = styled.div`
`)};
`
interface TransactionDetailsProps {
export interface SlippageTabsProps {
rawSlippage: number
setRawSlippage: (rawSlippage: number) => void
deadline: number
setDeadline: (deadline: number) => void
}
export default function TransactionDetails({
setRawSlippage,
rawSlippage,
deadline,
setDeadline
}: TransactionDetailsProps) {
export default function SlippageTabs({ setRawSlippage, rawSlippage, deadline, setDeadline }: SlippageTabsProps) {
const [activeIndex, setActiveIndex] = useState(2)
const [warningType, setWarningType] = useState(WARNING_TYPE.none)
@ -250,142 +245,138 @@ export default function TransactionDetails({
}
})
const dropDownContent = () => {
return (
<>
<SlippageSelector>
<RowBetween>
<Option
onClick={() => {
setFromFixed(1, 0.1)
}}
active={activeIndex === 1}
>
0.1%
</Option>
<Option
onClick={() => {
setFromFixed(2, 0.5)
}}
active={activeIndex === 2}
>
0.5%
</Option>
<Option
onClick={() => {
setFromFixed(3, 1)
}}
active={activeIndex === 3}
>
1%
</Option>
<OptionCustom
active={activeIndex === 4}
warning={
warningType !== WARNING_TYPE.none &&
warningType !== WARNING_TYPE.emptyInput &&
warningType !== WARNING_TYPE.riskyEntryLow
}
onClick={() => {
setFromCustom()
}}
>
<RowBetween>
{!(warningType === WARNING_TYPE.none || warningType === WARNING_TYPE.emptyInput) && (
<span
role="img"
aria-label="warning"
style={{
color:
warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: warningType === WARNING_TYPE.riskyEntryLow
? '#F3841E'
: ''
}}
>
</span>
)}
<Input
tabIndex={-1}
ref={inputRef}
active={activeIndex === 4}
placeholder={
activeIndex === 4
? !!userInput
? ''
: '0'
: activeIndex !== 4 && userInput !== ''
? 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'
: ''
}
return (
<>
<SlippageSelector>
<RowBetween>
<Option
onClick={() => {
setFromFixed(1, 0.1)
}}
active={activeIndex === 1}
>
0.1%
</Option>
<Option
onClick={() => {
setFromFixed(2, 0.5)
}}
active={activeIndex === 2}
>
0.5%
</Option>
<Option
onClick={() => {
setFromFixed(3, 1)
}}
active={activeIndex === 3}
>
1%
</Option>
<OptionCustom
active={activeIndex === 4}
warning={
warningType !== WARNING_TYPE.none &&
warningType !== WARNING_TYPE.emptyInput &&
warningType !== WARNING_TYPE.riskyEntryLow
}
onClick={() => {
setFromCustom()
}}
>
<RowBetween>
{!(warningType === WARNING_TYPE.none || warningType === WARNING_TYPE.emptyInput) && (
<span
role="img"
aria-label="warning"
style={{
color:
warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: warningType === WARNING_TYPE.riskyEntryLow
? '#F3841E'
: ''
}}
>
%
</Percent>
</RowBetween>
</OptionCustom>
</RowBetween>
<RowBetween>
<BottomError
show={activeIndex === 4}
color={
warningType === WARNING_TYPE.emptyInput
? '#565A69'
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: warningType === WARNING_TYPE.riskyEntryLow
? '#F3841E'
: ''
}
>
{warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage'}
{warningType === WARNING_TYPE.invalidEntryBound && 'Please select a value no greater than 50%'}
{warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun'}
{warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail'}
</BottomError>
</RowBetween>
</SlippageSelector>
<AutoColumn gap="sm">
<RowFixed padding={'0 20px'}>
<TYPE.body fontSize={14}>Deadline</TYPE.body>
<QuestionHelper text="Deadline in minutes. If your transaction takes longer than this it will revert." />
</RowFixed>
<RowFixed padding={'0 20px'}>
<OptionCustom style={{ width: '80px' }}>
</span>
)}
<Input
tabIndex={-1}
placeholder={'' + deadlineInput}
value={deadlineInput}
onChange={parseCustomDeadline}
ref={inputRef}
active={activeIndex === 4}
placeholder={
activeIndex === 4
? !!userInput
? ''
: '0'
: activeIndex !== 4 && userInput !== ''
? userInput
: 'Custom'
}
value={activeIndex === 4 ? userInput : ''}
onChange={parseInput}
color={
warningType === WARNING_TYPE.emptyInput
? ''
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: ''
}
/>
</OptionCustom>
<TYPE.body style={{ paddingLeft: '8px' }} fontSize={14}>
minutes
</TYPE.body>
</RowFixed>
</AutoColumn>
</>
)
}
return dropDownContent()
<Percent
color={
activeIndex !== 4
? 'faded'
: warningType === WARNING_TYPE.riskyEntryHigh || warningType === WARNING_TYPE.invalidEntryBound
? 'red'
: ''
}
>
%
</Percent>
</RowBetween>
</OptionCustom>
</RowBetween>
<RowBetween>
<BottomError
show={activeIndex === 4}
color={
warningType === WARNING_TYPE.emptyInput
? '#565A69'
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: warningType === WARNING_TYPE.riskyEntryLow
? '#F3841E'
: ''
}
>
{warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage'}
{warningType === WARNING_TYPE.invalidEntryBound && 'Please select a value no greater than 50%'}
{warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun'}
{warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail'}
</BottomError>
</RowBetween>
</SlippageSelector>
<AutoColumn gap="sm">
<RowFixed padding={'0 20px'}>
<TYPE.body fontSize={14}>Deadline</TYPE.body>
<QuestionHelper text="Deadline in minutes. If your transaction takes longer than this it will revert." />
</RowFixed>
<RowFixed padding={'0 20px'}>
<OptionCustom style={{ width: '80px' }}>
<Input
tabIndex={-1}
placeholder={'' + deadlineInput}
value={deadlineInput}
onChange={parseCustomDeadline}
/>
</OptionCustom>
<TYPE.body style={{ paddingLeft: '8px' }} fontSize={14}>
minutes
</TYPE.body>
</RowFixed>
</AutoColumn>
</>
)
}

@ -132,7 +132,7 @@ interface WarningCardProps {
currency: string
}
function WarningCard({ onDismiss, urlAddedTokens, currency }: WarningCardProps) {
export default function WarningCard({ onDismiss, urlAddedTokens, currency }: WarningCardProps) {
const [showPopup, setPopup] = useState<boolean>(false)
const { chainId } = useWeb3React()
const { symbol: inputSymbol, name: inputName } = useToken(currency)
@ -183,5 +183,3 @@ function WarningCard({ onDismiss, urlAddedTokens, currency }: WarningCardProps)
</Wrapper>
)
}
export default WarningCard

@ -0,0 +1,91 @@
import { Trade, TradeType } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { ChevronUp } from 'react-feather'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { Field } from '../../state/swap/actions'
import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
import { AutoColumn } from '../Column'
import { SectionBreak } from './styleds'
import QuestionHelper from '../Question'
import { RowBetween, RowFixed } from '../Row'
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
import FormattedPriceImpact from './FormattedPriceImpact'
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
trade: Trade
onDismiss: () => void
}
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
const theme = useContext(ThemeContext)
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, slippageTabProps.rawSlippage)
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>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
{isExactIn ? 'Minimum received' : 'Maximum sold'}
</TYPE.black>
<QuestionHelper
text={
isExactIn
? 'Price can change between when a transaction is submitted and when it is executed. This is the minimum amount you will receive. A worse rate will cause your transaction to revert.'
: 'Price can change between when a transaction is submitted and when it is executed. This is the maximum amount you will pay. A worse rate will cause your transaction to revert.'
}
/>
</RowFixed>
<RowFixed>
<TYPE.black color={theme.text1} fontSize={14}>
{isExactIn
? `${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} ${trade.outputAmount.token.symbol}` ?? '-'
: `${slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4)} ${trade.inputAmount.token.symbol}` ?? '-'}
</TYPE.black>
</RowFixed>
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Price Impact
</TYPE.black>
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
</RowFixed>
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Liquidity Provider Fee
</TYPE.black>
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed>
<TYPE.black fontSize={14} color={theme.text1}>
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${trade.inputAmount.token.symbol}` : '-'}
</TYPE.black>
</RowBetween>
</AutoColumn>
<SectionBreak />
<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} />
</AutoColumn>
)
}

@ -0,0 +1,47 @@
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'
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>
{showAdvanced ? (
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
) : (
<CursorPointer>
<RowBetween onClick={() => setShowAdvanced(true)} padding={'8px 20px'} id="show-advanced">
<Text fontSize={16} fontWeight={500} style={{ userSelect: 'none' }}>
Show Advanced
</Text>
<ChevronDown color={theme.text2} />
</RowBetween>
</CursorPointer>
)}
<FixedBottom>
<AutoColumn gap="lg">
{severity > 2 && <PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />}
</AutoColumn>
</FixedBottom>
</AdvancedDropwdown>
)
}

@ -0,0 +1,13 @@
import { Percent } from '@uniswap/sdk'
import React from 'react'
import { ONE_BIPS } from '../../constants'
import { warningServerity } from '../../utils/prices'
import { ErrorText } from './styleds'
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
return (
<ErrorText fontWeight={500} fontSize={14} severity={warningServerity(priceImpact)}>
{priceImpact?.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact?.toFixed(2)}%` ?? '-'}
</ErrorText>
)
}

@ -0,0 +1,30 @@
import { Percent } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { YellowCard } from '../Card'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
export function PriceSlippageWarningCard({ priceSlippage }: { priceSlippage: Percent }) {
const theme = useContext(ThemeContext)
return (
<YellowCard style={{ padding: '20px', paddingTop: '10px' }}>
<AutoColumn gap="md">
<RowBetween>
<RowFixed style={{ paddingTop: '8px' }}>
<span role="img" aria-label="warning">
</span>{' '}
<Text fontWeight={500} marginLeft="4px" color={theme.text1}>
Price Warning
</Text>
</RowFixed>
</RowBetween>
<Text lineHeight="145.23%;" fontSize={16} fontWeight={400} color={theme.text1}>
This trade will move the price by ~{priceSlippage.toFixed(2)}%.
</Text>
</AutoColumn>
</YellowCard>
)
}

@ -0,0 +1,119 @@
import { Percent, TokenAmount, Trade, TradeType } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { Repeat } from 'react-feather'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { Field } from '../../state/swap/actions'
import { TYPE } from '../../theme'
import { formatExecutionPrice } from '../../utils/prices'
import { ButtonError } from '../Button'
import { AutoColumn } from '../Column'
import QuestionHelper from '../Question'
import { AutoRow, RowBetween, RowFixed } from '../Row'
import FormattedPriceImpact from './FormattedPriceImpact'
import { StyledBalanceMaxMini } from './styleds'
export default function SwapModalFooter({
trade,
showInverted,
setShowInverted,
severity,
slippageAdjustedAmounts,
onSwap,
parsedAmounts,
realizedLPFee,
priceImpactWithoutFee,
confirmText
}: {
trade?: Trade
showInverted: boolean
setShowInverted: (inverted: boolean) => void
severity: number
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount }
onSwap: () => any
parsedAmounts?: { [field in Field]?: TokenAmount }
realizedLPFee?: TokenAmount
priceImpactWithoutFee?: Percent
confirmText: string
}) {
const theme = useContext(ThemeContext)
return (
<>
<AutoColumn gap="0px">
<RowBetween align="center">
<Text fontWeight={400} fontSize={14} color={theme.text2}>
Price
</Text>
<Text
fontWeight={500}
fontSize={14}
color={theme.text1}
style={{
justifyContent: 'center',
alignItems: 'center',
display: 'flex',
textAlign: 'right',
paddingLeft: '10px'
}}
>
{formatExecutionPrice(trade, showInverted)}
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
<Repeat size={14} />
</StyledBalanceMaxMini>
</Text>
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
{trade?.tradeType === TradeType.EXACT_INPUT ? 'Min sent' : 'Maximum sold'}
</TYPE.black>
<QuestionHelper text="A boundary is set so you are protected from large price movements after you submit your trade." />
</RowFixed>
<RowFixed>
<TYPE.black fontSize={14}>
{trade?.tradeType === TradeType.EXACT_INPUT
? slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4) ?? '-'
: slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4) ?? '-'}
</TYPE.black>
{parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && (
<TYPE.black fontSize={14} marginLeft={'4px'}>
{trade?.tradeType === TradeType.EXACT_INPUT
? parsedAmounts[Field.OUTPUT]?.token?.symbol
: parsedAmounts[Field.INPUT]?.token?.symbol}
</TYPE.black>
)}
</RowFixed>
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black color={theme.text2} fontSize={14} fontWeight={400}>
Price Impact
</TYPE.black>
<QuestionHelper text="The difference between the market price and your price due to trade size." />
</RowFixed>
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Liquidity Provider Fee
</TYPE.black>
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed>
<TYPE.black fontSize={14}>
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade?.inputAmount?.token?.symbol : '-'}
</TYPE.black>
</RowBetween>
</AutoColumn>
<AutoRow>
<ButtonError onClick={onSwap} error={severity > 2} style={{ margin: '10px 0 0 0' }} id="confirm-swap-or-send">
<Text fontSize={20} fontWeight={500}>
{confirmText}
</Text>
</ButtonError>
</AutoRow>
</>
)
}

@ -0,0 +1,75 @@
import { Token, TokenAmount } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { ArrowDown } from 'react-feather'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { Field } from '../../state/swap/actions'
import { TYPE } from '../../theme'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import TokenLogo from '../TokenLogo'
import { TruncatedText } from './styleds'
export default function SwapModalHeader({
formattedAmounts,
tokens,
slippageAdjustedAmounts,
priceImpactSeverity,
independentField
}: {
formattedAmounts?: { [field in Field]?: string }
tokens?: { [field in Field]?: Token }
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount }
priceImpactSeverity: number
independentField: Field
}) {
const theme = useContext(ThemeContext)
return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500}>
{!!formattedAmounts[Field.INPUT] && formattedAmounts[Field.INPUT]}
</TruncatedText>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.INPUT]?.symbol || ''}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<ArrowDown size="16" color={theme.text2} />
</RowFixed>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
{!!formattedAmounts[Field.OUTPUT] && formattedAmounts[Field.OUTPUT]}
</TruncatedText>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.OUTPUT]?.symbol || ''}
</Text>
</RowFixed>
</RowBetween>
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '12px 0 0 0px' }}>
{independentField === Field.INPUT ? (
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Output is estimated. You will receive at least `}
<b>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}{' '}
</b>
{' or the transaction will revert.'}
</TYPE.italic>
) : (
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Input is estimated. You will sell at most `}
<b>
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {tokens[Field.INPUT]?.symbol}
</b>
{' or the transaction will revert.'}
</TYPE.italic>
)}
</AutoColumn>
</AutoColumn>
)
}

@ -0,0 +1,55 @@
import { TokenAmount } from '@uniswap/sdk'
import React from 'react'
import { Text } from 'rebass'
import { useWeb3React } from '../../hooks'
import { Link, TYPE } from '../../theme'
import { getEtherscanLink } from '../../utils'
import Copy from '../AccountDetails/Copy'
import { AutoColumn } from '../Column'
import { AutoRow, RowBetween } from '../Row'
import TokenLogo from '../TokenLogo'
export function TransferModalHeader({
recipient,
ENSName,
amount
}: {
recipient: string
ENSName: string
amount: TokenAmount
}) {
const { chainId } = useWeb3React()
return (
<AutoColumn gap="lg" style={{ marginTop: '40px' }}>
<RowBetween>
<Text fontSize={36} fontWeight={500}>
{amount?.toSignificant(6)} {amount?.token?.symbol}
</Text>
<TokenLogo address={amount?.token?.address} size={'30px'} />
</RowBetween>
<TYPE.darkGray fontSize={20}>To</TYPE.darkGray>
{ENSName ? (
<AutoColumn gap="lg">
<TYPE.blue fontSize={36}>{ENSName}</TYPE.blue>
<AutoRow gap="10px">
<Link href={getEtherscanLink(chainId, ENSName, 'address')}>
<TYPE.blue fontSize={18}>
{recipient?.slice(0, 8)}...{recipient?.slice(34, 42)}
</TYPE.blue>
</Link>
<Copy toCopy={recipient} />
</AutoRow>
</AutoColumn>
) : (
<AutoRow gap="10px">
<Link href={getEtherscanLink(chainId, recipient, 'address')}>
<TYPE.blue fontSize={36}>
{recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}
</TYPE.blue>
</Link>
<Copy toCopy={recipient} />
</AutoRow>
)}
</AutoColumn>
)
}

@ -0,0 +1,26 @@
import { Trade } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { V1_TRADE_LINK_THRESHOLD } from '../../constants'
import { useV1TradeLinkIfBetter } from '../../data/V1'
import { Link } from '../../theme'
import { YellowCard } from '../Card'
import { AutoColumn } from '../Column'
export default function V1TradeLink({ bestV2Trade }: { bestV2Trade?: Trade }) {
const v1TradeLinkIfBetter = useV1TradeLinkIfBetter(bestV2Trade, V1_TRADE_LINK_THRESHOLD)
const theme = useContext(ThemeContext)
return v1TradeLinkIfBetter ? (
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
There is a better price for this trade on
<Link href={v1TradeLinkIfBetter}>
<b> Uniswap V1 </b>
</Link>
</Text>
</AutoColumn>
</YellowCard>
) : null
}

@ -55,9 +55,9 @@ export const BottomGrouping = styled.div`
position: relative;
`
export const ErrorText = styled(Text)<{ warningLow?: boolean; warningMedium?: boolean; warningHigh?: boolean }>`
color: ${({ theme, warningLow, warningMedium, warningHigh }) =>
warningHigh ? theme.red1 : warningMedium ? theme.yellow2 : warningLow ? theme.green1 : theme.text1};
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};
`
export const InputGroup = styled(AutoColumn)`
@ -75,29 +75,6 @@ export const StyledNumerical = styled(NumericalInput)`
color: ${({ theme }) => theme.text4};
}
`
export const MaxButton = styled.button`
position: absolute;
right: 70px;
padding: 0.25rem 0.35rem;
background-color: ${({ theme }) => theme.primary5};
border: 1px solid ${({ theme }) => theme.primary5};
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
margin-right: 0.5rem;
color: ${({ theme }) => theme.primaryText1};
:hover {
border: 1px solid ${({ theme }) => theme.primary1};
}
:focus {
border: 1px solid ${({ theme }) => theme.primary1};
outline: none;
}
`
export const StyledBalanceMaxMini = styled.button<{ active?: boolean }>`
height: 22px;
width: 22px;

@ -1,5 +1,5 @@
import { injected, walletconnect, walletlink, fortmatic, portis } from '../connectors'
import { ChainId, WETH, Token } from '@uniswap/sdk'
import { ChainId, Token, WETH, JSBI, Percent } from '@uniswap/sdk'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
export const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
@ -103,3 +103,18 @@ export const SUPPORTED_WALLETS =
}
export const NetworkContextName = 'NETWORK'
// default allowed slippage, in bips
export const INITIAL_ALLOWED_SLIPPAGE = 50
// 15 minutes, denominated in seconds
export const DEFAULT_DEADLINE_FROM_NOW = 60 * 15
// one basis point
export const ONE_BIPS = new Percent(JSBI.BigInt(1), JSBI.BigInt(10000))
// used for warning states
export const ALLOWED_SLIPPAGE_LOW: Percent = new Percent(JSBI.BigInt(100), JSBI.BigInt(10000))
export const ALLOWED_SLIPPAGE_MEDIUM: Percent = new Percent(JSBI.BigInt(500), JSBI.BigInt(10000))
export const ALLOWED_SLIPPAGE_HIGH: Percent = new Percent(JSBI.BigInt(1000), JSBI.BigInt(10000))
// used to ensure the user doesn't send so much ETH so they end up with <.01
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
export const V1_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(50), JSBI.BigInt(10000))

@ -1,3 +1,4 @@
import { Contract } from '@ethersproject/contracts'
import { Web3Provider } from '@ethersproject/providers'
import { useState, useMemo, useCallback, useEffect, useRef } from 'react'
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
@ -190,7 +191,7 @@ export function useContract(address, ABI, withSignerIfPossible = true) {
}
// returns null on errors
export function useTokenContract(tokenAddress, withSignerIfPossible = true) {
export function useTokenContract(tokenAddress: string, withSignerIfPossible = true): Contract {
const { library, account } = useWeb3React()
return useMemo(() => {

@ -0,0 +1,61 @@
import { MaxUint256 } from '@ethersproject/constants'
import { Trade, WETH } from '@uniswap/sdk'
import { useCallback, useMemo } from 'react'
import { ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
import { Field } from '../state/swap/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin } from '../utils'
import { useTokenContract, useWeb3React } from './index'
// returns a function to approve the amount required to execute a trade if necessary, otherwise null
export function useApproveCallback(trade?: Trade, allowedSlippage = 0): [undefined | boolean, () => Promise<void>] {
const { account, chainId } = useWeb3React()
const currentAllowance = useTokenAllowance(trade?.inputAmount?.token, account, ROUTER_ADDRESS)
const slippageAdjustedAmountIn = useMemo(() => computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT], [
trade,
allowedSlippage
])
const mustApprove = useMemo(() => {
// we treat WETH as ETH which requires no approvals
if (trade?.inputAmount?.token?.equals(WETH[chainId])) return false
// return undefined if we don't have enough data to know whether or not we need to approve
if (!currentAllowance) return undefined
// slippageAdjustedAmountIn will be defined if currentAllowance is
return currentAllowance.lessThan(slippageAdjustedAmountIn)
}, [trade, chainId, currentAllowance, slippageAdjustedAmountIn])
const tokenContract = useTokenContract(trade?.inputAmount?.token?.address)
const addTransaction = useTransactionAdder()
const approve = useCallback(async (): Promise<void> => {
if (!mustApprove) return
let useUserBalance = false
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, slippageAdjustedAmountIn.raw.toString())
})
return tokenContract
.approve(ROUTER_ADDRESS, useUserBalance ? slippageAdjustedAmountIn.raw.toString() : MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas)
})
.then(response => {
addTransaction(response, {
summary: 'Approve ' + trade?.inputAmount?.token?.symbol,
approvalOfToken: trade?.inputAmount?.token?.symbol
})
})
.catch(error => {
console.debug('Failed to approve token', error)
throw error
})
}, [mustApprove, tokenContract, slippageAdjustedAmountIn, trade, addTransaction])
return [mustApprove, approve]
}

@ -0,0 +1,64 @@
import { BigNumber } from '@ethersproject/bignumber'
import { WETH, TokenAmount, JSBI } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useTransactionAdder } from '../state/transactions/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../state/wallet/hooks'
import { calculateGasMargin, getSigner, isAddress } from '../utils'
import { useENSName, useTokenContract, useWeb3React } from './index'
// returns a callback for sending a token amount, treating WETH as ETH
// returns null with invalid arguments
export function useSendCallback(amount?: TokenAmount, recipient?: string): null | (() => Promise<string>) {
const { library, account, chainId } = useWeb3React()
const addTransaction = useTransactionAdder()
const ensName = useENSName(recipient)
const tokenContract = useTokenContract(amount?.token?.address)
const balance = useTokenBalanceTreatingWETHasETH(account, amount?.token)
return useMemo(() => {
if (!amount) return null
if (!amount.greaterThan(JSBI.BigInt(0))) return null
if (!isAddress(recipient)) return null
if (!balance) return null
if (balance.lessThan(amount)) return null
const token = amount?.token
return async function onSend(): Promise<string> {
if (token.equals(WETH[chainId])) {
return getSigner(library, account)
.sendTransaction({ to: recipient, value: BigNumber.from(amount.raw.toString()) })
.then(response => {
addTransaction(response, {
summary: 'Send ' + amount.toSignificant(3) + ' ' + token?.symbol + ' to ' + (ensName ?? recipient)
})
return response.hash
})
.catch(error => {
console.error('Failed to transfer ETH', error)
throw error
})
} else {
return tokenContract.estimateGas
.transfer(recipient, amount.raw.toString())
.then(estimatedGasLimit =>
tokenContract
.transfer(recipient, amount.raw.toString(), {
gasLimit: calculateGasMargin(estimatedGasLimit)
})
.then(response => {
addTransaction(response, {
summary: 'Send ' + amount.toSignificant(3) + ' ' + token.symbol + ' to ' + (ensName ?? recipient)
})
return response.hash
})
)
.catch(error => {
console.error('Failed token transfer', error)
throw error
})
}
}
}, [addTransaction, library, account, chainId, amount, ensName, recipient, tokenContract, balance])
}

@ -0,0 +1,192 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Contract } from '@ethersproject/contracts'
import { Token, Trade, TradeType, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
import { Field } from '../state/swap/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
import { useENSName, useWeb3React } from './index'
enum SwapType {
EXACT_TOKENS_FOR_TOKENS,
EXACT_TOKENS_FOR_ETH,
EXACT_ETH_FOR_TOKENS,
TOKENS_FOR_EXACT_TOKENS,
TOKENS_FOR_EXACT_ETH,
ETH_FOR_EXACT_TOKENS
}
function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, chainId: number): SwapType {
if (isExactIn) {
if (tokens[Field.INPUT]?.equals(WETH[chainId])) {
return SwapType.EXACT_ETH_FOR_TOKENS
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId])) {
return SwapType.EXACT_TOKENS_FOR_ETH
} else {
return SwapType.EXACT_TOKENS_FOR_TOKENS
}
} else {
if (tokens[Field.INPUT]?.equals(WETH[chainId])) {
return SwapType.ETH_FOR_EXACT_TOKENS
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId])) {
return SwapType.TOKENS_FOR_EXACT_ETH
} else {
return SwapType.TOKENS_FOR_EXACT_TOKENS
}
}
}
// returns a function that will execute a swap, if the parameters are all valid
// and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback(
trade?: Trade, // trade to execute, required
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips, optional
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now, optional
to?: string // recipient of output, optional
): null | (() => Promise<string>) {
const { account, chainId, library } = useWeb3React()
const inputAllowance = useTokenAllowance(trade?.inputAmount?.token, account, ROUTER_ADDRESS)
const addTransaction = useTransactionAdder()
const recipient = to ? isAddress(to) : account
const ensName = useENSName(to)
return useMemo(() => {
if (!trade) return null
if (!recipient) return null
// will always be defined
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage)
// no allowance
if (
!trade.inputAmount.token.equals(WETH[chainId]) &&
(!inputAllowance || slippageAdjustedAmounts[Field.INPUT].greaterThan(inputAllowance))
) {
return null
}
return async function onSwap() {
const routerContract: Contract = getRouterContract(chainId, library, account)
const path = trade.route.path.map(t => t.address)
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
const swapType = getSwapType(
{ [Field.INPUT]: trade.inputAmount.token, [Field.OUTPUT]: trade.outputAmount.token },
trade.tradeType === TradeType.EXACT_INPUT,
chainId
)
let estimate, method, args, value
switch (swapType) {
case SwapType.EXACT_TOKENS_FOR_TOKENS:
estimate = routerContract.estimateGas.swapExactTokensForTokens
method = routerContract.swapExactTokensForTokens
args = [
slippageAdjustedAmounts[Field.INPUT].raw.toString(),
slippageAdjustedAmounts[Field.OUTPUT].raw.toString(),
path,
account,
deadlineFromNow
]
value = null
break
case SwapType.TOKENS_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapTokensForExactTokens
method = routerContract.swapTokensForExactTokens
args = [
slippageAdjustedAmounts[Field.OUTPUT].raw.toString(),
slippageAdjustedAmounts[Field.INPUT].raw.toString(),
path,
account,
deadlineFromNow
]
value = null
break
case SwapType.EXACT_ETH_FOR_TOKENS:
estimate = routerContract.estimateGas.swapExactETHForTokens
method = routerContract.swapExactETHForTokens
args = [slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), path, account, deadlineFromNow]
value = BigNumber.from(slippageAdjustedAmounts[Field.INPUT].raw.toString())
break
case SwapType.TOKENS_FOR_EXACT_ETH:
estimate = routerContract.estimateGas.swapTokensForExactETH
method = routerContract.swapTokensForExactETH
args = [
slippageAdjustedAmounts[Field.OUTPUT].raw.toString(),
slippageAdjustedAmounts[Field.INPUT].raw.toString(),
path,
account,
deadlineFromNow
]
value = null
break
case SwapType.EXACT_TOKENS_FOR_ETH:
estimate = routerContract.estimateGas.swapExactTokensForETH
method = routerContract.swapExactTokensForETH
args = [
slippageAdjustedAmounts[Field.INPUT].raw.toString(),
slippageAdjustedAmounts[Field.OUTPUT].raw.toString(),
path,
account,
deadlineFromNow
]
value = null
break
case SwapType.ETH_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapETHForExactTokens
method = routerContract.swapETHForExactTokens
args = [slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), path, account, deadlineFromNow]
value = BigNumber.from(slippageAdjustedAmounts[Field.INPUT].raw.toString())
break
}
return estimate(...args, value ? { value } : {})
.then(estimatedGasLimit =>
method(...args, {
...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit)
})
)
.then(response => {
if (recipient === account) {
addTransaction(response, {
summary:
'Swap ' +
slippageAdjustedAmounts[Field.INPUT].toSignificant(3) +
' ' +
trade.inputAmount.token.symbol +
' for ' +
slippageAdjustedAmounts[Field.OUTPUT].toSignificant(3) +
' ' +
trade.outputAmount.token.symbol
})
} else {
addTransaction(response, {
summary:
'Swap ' +
slippageAdjustedAmounts[Field.INPUT].toSignificant(3) +
' ' +
trade.inputAmount.token.symbol +
' for ' +
slippageAdjustedAmounts[Field.OUTPUT].toSignificant(3) +
' ' +
trade.outputAmount.token.symbol +
' to ' +
(ensName ?? recipient)
})
}
return response.hash
})
.catch(error => {
console.error(`Swap or gas estimate failed`, error)
throw error
})
}
}, [account, allowedSlippage, addTransaction, chainId, deadline, inputAllowance, library, trade, ensName, recipient])
}

@ -9,7 +9,7 @@ import NavigationTabs from '../components/NavigationTabs'
import Web3ReactManager from '../components/Web3ReactManager'
import Popups from '../components/Popups'
import { isAddress, getAllQueryParams } from '../utils'
import { isAddress } from '../utils'
import Swap from './Swap'
import Send from './Send'
@ -99,8 +99,6 @@ function GoogleAnalyticsReporter({ location: { pathname, search } }: RouteCompon
}
export default function App() {
const params = getAllQueryParams()
return (
<>
<Suspense fallback={null}>
@ -115,14 +113,12 @@ export default function App() {
<Web3ReactManager>
<Body>
<NavigationTabs />
{/* this Suspense is for route code-splitting */}
<Switch>
<Route exact strict path="/" render={() => <Redirect to="/swap" />} />
<Route exact strict path="/swap" component={() => <Swap params={params} />} />
<Route exact strict path="/send" component={() => <Send params={params} />} />
<Route exact strict path="/find" component={() => <Find />} />
<Route exact strict path="/create" component={() => <Create />} />
<Route exact strict path="/pool" component={() => <Pool />} />
<Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/send" component={Send} />
<Route exact strict path="/find" component={Find} />
<Route exact strict path="/create" component={Create} />
<Route exact strict path="/pool" component={Pool} />
<Route
exact
strict
@ -157,7 +153,7 @@ export default function App() {
}
}}
/>
<Redirect to="/" />
<Redirect to="/swap" />
</Switch>
</Body>
</Web3ReactManager>

@ -47,8 +47,8 @@ const FixedBottom = styled.div`
`
enum Field {
INPUT,
OUTPUT
INPUT = 'INPUT',
OUTPUT = 'OUTPUT'
}
interface AddState {
@ -155,7 +155,7 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
// get basic SDK entities
const tokens: { [field: number]: Token } = {
const tokens: { [field in Field]: Token } = {
[Field.INPUT]: useTokenByAddressAndAutomaticallyAdd(fieldData[Field.INPUT].address),
[Field.OUTPUT]: useTokenByAddressAndAutomaticallyAdd(fieldData[Field.OUTPUT].address)
}
@ -173,7 +173,7 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
(!!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
const userBalances: { [field: number]: TokenAmount } = {
const userBalances: { [field in Field]: TokenAmount } = {
[Field.INPUT]: useTokenBalanceTreatingWETHasETH(account, tokens[Field.INPUT]),
[Field.OUTPUT]: useTokenBalanceTreatingWETHasETH(account, tokens[Field.OUTPUT])
}
@ -443,7 +443,7 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
ReactGA.event({
category: 'Liquidity',
action: 'Add',
label: [tokens[Field.INPUT]?.symbol, tokens[Field.OUTPUT]?.symbol].join(';')
label: [tokens[Field.INPUT]?.symbol, tokens[Field.OUTPUT]?.symbol].join('/')
})
setTxHash(response.hash)
addTransaction(response, {
@ -697,7 +697,7 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
onMax={() => {
maxAmountInput && onMax(maxAmountInput.toExact(), Field.INPUT)
}}
atMax={atMaxAmountInput}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
pair={pair}
@ -714,7 +714,7 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
onMax={() => {
maxAmountOutput && onMax(maxAmountOutput?.toExact(), Field.OUTPUT)
}}
atMax={atMaxAmountOutput}
showMaxButton={!atMaxAmountOutput}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
pair={pair}

@ -37,9 +37,9 @@ const ALLOWED_SLIPPAGE = 50
const DEADLINE_FROM_NOW = 60 * 20
enum Field {
LIQUIDITY,
TOKEN0,
TOKEN1
LIQUIDITY = 'LIQUIDITY',
TOKEN0 = 'TOKEN0',
TOKEN1 = 'TOKEN1'
}
interface RemoveState {
@ -120,7 +120,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
const outputToken: Token = useToken(token1)
// get basic SDK entities
const tokens: { [field: number]: Token } = {
const tokens: { [field in Field]?: Token } = {
[Field.TOKEN0]: inputToken,
[Field.TOKEN1]: outputToken
}
@ -136,7 +136,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
const [state, dispatch] = useReducer(reducer, initializeRemoveState(userLiquidity?.toExact(), token0, token1))
const { independentField, typedValue } = state
const tokensDeposited: { [field: number]: TokenAmount } = {
const tokensDeposited: { [field in Field]?: TokenAmount } = {
[Field.TOKEN0]:
pair &&
totalPoolTokens &&
@ -164,7 +164,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
dispatch({ type: RemoveAction.TYPE, payload: { field, typedValue } })
}, [])
const parsedAmounts: { [field: number]: TokenAmount } = {}
const parsedAmounts: { [field in Field]?: TokenAmount } = {}
let poolTokenAmount
try {
if (typedValue !== '' && typedValue !== '.' && tokens[Field.TOKEN0] && tokens[Field.TOKEN1] && userLiquidity) {
@ -461,7 +461,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [tokens[Field.TOKEN0]?.symbol, tokens[Field.TOKEN1]?.symbol].join(';')
label: [tokens[Field.TOKEN0]?.symbol, tokens[Field.TOKEN1]?.symbol].join('/')
})
setPendingConfirmation(false)
setTxHash(response.hash)
@ -680,7 +680,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onUserInput}
onMax={onMax}
atMax={atMaxAmount}
showMaxButton={!atMaxAmount}
disableTokenSelect
token={pair?.liquidityToken}
isExchange={true}
@ -695,7 +695,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
value={formattedAmounts[Field.TOKEN0]}
onUserInput={onUserInput}
onMax={onMax}
atMax={atMaxAmount}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN0]}
label={'Output'}
disableTokenSelect
@ -709,7 +709,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
value={formattedAmounts[Field.TOKEN1]}
onUserInput={onUserInput}
onMax={onMax}
atMax={atMaxAmount}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN1]}
label={'Output'}
disableTokenSelect

@ -1,8 +1,526 @@
import React from 'react'
import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useContext, useEffect, useState } from 'react'
import { ArrowDown, Repeat } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import AddressInputPanel from '../../components/AddressInputPanel'
import { ButtonError, ButtonLight, ButtonPrimary, ButtonSecondary } from '../../components/Button'
import Card, { BlueCard, GreyCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import QuestionHelper from '../../components/Question'
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import FormattedPriceImpact from '../../components/swap/FormattedPriceImpact'
import SwapModalFooter from '../../components/swap/SwapModalFooter'
import {
ArrowWrapper,
BottomGrouping,
Dots,
InputGroup,
StyledBalanceMaxMini,
StyledNumerical,
Wrapper
} from '../../components/swap/styleds'
import { TransferModalHeader } from '../../components/swap/TransferModalHeader'
import V1TradeLink from '../../components/swap/V1TradeLink'
import TokenLogo from '../../components/TokenLogo'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
import { useWeb3React } from '../../hooks'
import { useApproveCallback } from '../../hooks/useApproveCallback'
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 { useHasPendingApproval } from '../../state/transactions/hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { Link } from '../../theme/components'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
import ExchangePage from '../../components/ExchangePage'
import { QueryParams } from '../../utils'
export default function Send({ history, location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search)
export default function Send({ params }: { params: QueryParams }) {
return <ExchangePage sendingInput={true} params={params} />
// text translation
// const { t } = useTranslation()
const { chainId, account } = useWeb3React()
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
// sending state
const [sendingWithSwap, setSendingWithSwap] = useState<boolean>(false)
const [recipient, setRecipient] = useState<string>('')
const [ENS, setENS] = useState<string>('')
const [recipientError, setRecipientError] = useState<string | null>('Enter a Recipient')
// trade details, check query params for initial state
const { independentField, typedValue } = useSwapState()
const { parsedAmounts, bestTrade, tokenBalances, tokens, error: swapError } = useDerivedSwapInfo()
const isSwapValid = !swapError && !recipientError && bestTrade
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirmed
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
const route = bestTrade?.route
const userHasSpecifiedInputOutput =
!!tokens[Field.INPUT] &&
!!tokens[Field.OUTPUT] &&
!!parsedAmounts[independentField] &&
parsedAmounts[independentField].greaterThan(JSBI.BigInt(0))
const noRoute = !route
// check whether the user has approved the router on the input token
const [mustApprove, approveCallback] = useApproveCallback(bestTrade, allowedSlippage)
const pendingApprovalInput = useHasPendingApproval(tokens[Field.INPUT]?.address)
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
}
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(bestTrade)
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
// reset field if sending with with swap is cancled
useEffect(() => {
if (!sendingWithSwap) {
onTokenSelection(Field.OUTPUT, null)
}
}, [onTokenSelection, sendingWithSwap])
const maxAmountInput: TokenAmount =
!!tokenBalances[Field.INPUT] &&
!!tokens[Field.INPUT] &&
!!WETH[chainId] &&
tokenBalances[Field.INPUT].greaterThan(
new TokenAmount(tokens[Field.INPUT], tokens[Field.INPUT].equals(WETH[chainId]) ? MIN_ETH : '0')
)
? tokens[Field.INPUT].equals(WETH[chainId])
? tokenBalances[Field.INPUT].subtract(new TokenAmount(WETH[chainId], MIN_ETH))
: tokenBalances[Field.INPUT]
: undefined
const atMaxAmountInput: boolean =
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
// reset modal state when closed
function resetModal() {
// clear input if txn submitted
if (!pendingConfirmation) {
onUserInput(Field.INPUT, '')
}
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowAdvanced(false)
}
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline, recipient)
function onSwap() {
setAttemptingTxn(true)
swapCallback().then(hash => {
setTxHash(hash)
setPendingConfirmation(false)
ReactGA.event({
category: 'Send',
action: recipient === account ? 'Swap w/o Send' : 'Swap w/ Send',
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join(';')
})
})
}
const sendCallback = useSendCallback(parsedAmounts?.[Field.INPUT], recipient)
const isSendValid = sendCallback !== null && (sendingWithSwap === false || mustApprove === false)
async function onSend() {
setAttemptingTxn(true)
sendCallback()
.then(hash => {
setTxHash(hash)
ReactGA.event({ category: 'Swap', action: 'Send', label: tokens[Field.INPUT]?.symbol })
setPendingConfirmation(false)
})
.catch(() => {
resetModal()
setShowConfirm(false)
})
}
const [showInverted, setShowInverted] = useState<boolean>(false)
// warnings on slippage
const severity = !sendingWithSwap ? 0 : warningServerity(priceImpactWithoutFee)
function modalHeader() {
if (!sendingWithSwap) {
return <TransferModalHeader amount={parsedAmounts?.[Field.INPUT]} ENSName={ENS} recipient={recipient} />
}
if (sendingWithSwap) {
return (
<AutoColumn gap="lg" style={{ marginTop: '40px' }}>
<AutoColumn gap="sm">
<AutoRow gap="10px">
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'30px'} />
<Text fontSize={36} fontWeight={500}>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} {tokens[Field.OUTPUT]?.symbol}
</Text>
</AutoRow>
<BlueCard>
Via {parsedAmounts[Field.INPUT]?.toSignificant(4)} {tokens[Field.INPUT]?.symbol} swap
</BlueCard>
</AutoColumn>
<AutoColumn gap="sm">
<TYPE.darkGray fontSize={20}>To</TYPE.darkGray>
<TYPE.blue fontSize={36}>
{recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}
</TYPE.blue>
</AutoColumn>
</AutoColumn>
)
}
}
function modalBottom() {
if (!sendingWithSwap) {
return (
<AutoColumn>
<ButtonPrimary onClick={onSend} id="confirm-send">
<Text color="white" fontSize={20}>
Confirm send
</Text>
</ButtonPrimary>
</AutoColumn>
)
}
if (sendingWithSwap) {
return (
<SwapModalFooter
trade={bestTrade}
onSwap={onSwap}
setShowInverted={setShowInverted}
severity={severity}
showInverted={showInverted}
slippageAdjustedAmounts={slippageAdjustedAmounts}
priceImpactWithoutFee={priceImpactWithoutFee}
parsedAmounts={parsedAmounts}
realizedLPFee={realizedLPFee}
confirmText={severity > 2 ? 'Send Anyway' : 'Confirm Send'}
/>
)
}
}
// text to show while loading
const pendingText: string = sendingWithSwap
? `Sending ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol} to ${recipient}`
: `Sending ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${tokens[Field.INPUT]?.symbol} to ${recipient}`
const allBalances = useAllTokenBalancesTreatingWETHasETH() // only for 0 balance token selection behavior
const swapState = useSwapState()
function _onTokenSelect(address: string) {
// if no user balance - switch view to a send with swap
const hasBalance = allBalances?.[account]?.[address]?.greaterThan('0') ?? false
if (!hasBalance) {
onTokenSelection(
Field.INPUT,
swapState[Field.INPUT]?.address === address ? null : swapState[Field.INPUT]?.address
)
onTokenSelection(Field.OUTPUT, address)
setSendingWithSwap(true)
} else {
onTokenSelection(Field.INPUT, address)
}
}
function _onRecipient(result) {
if (result.address) {
setRecipient(result.address)
} else {
setRecipient('')
}
if (result.name) {
setENS(result.name)
}
}
const sendAmountError =
!sendingWithSwap && JSBI.equal(parsedAmounts?.[Field.INPUT]?.raw ?? JSBI.BigInt(0), JSBI.BigInt(0))
? 'Enter an amount'
: null
return (
<Wrapper id="send-page">
<ConfirmationModal
isOpen={showConfirm}
title={sendingWithSwap ? 'Confirm swap and send' : 'Confirm Send'}
onDismiss={() => {
resetModal()
setShowConfirm(false)
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
/>
{!sendingWithSwap && (
<AutoColumn justify="center" style={{ marginBottom: '1rem' }}>
<InputGroup gap="lg" justify="center">
<StyledNumerical
id="sending-no-swap-input"
value={formattedAmounts[Field.INPUT]}
onUserInput={val => onUserInput(Field.INPUT, val)}
/>
<CurrencyInputPanel
field={Field.INPUT}
value={formattedAmounts[Field.INPUT]}
onUserInput={(field, val) => onUserInput(Field.INPUT, val)}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onTokenSelection={address => _onTokenSelect(address)}
hideBalance={true}
hideInput={true}
showSendWithSwap={true}
label={''}
id="swap-currency-input"
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
/>
</InputGroup>
<RowBetween style={{ width: 'fit-content' }}>
<ButtonSecondary
width="fit-content"
style={{ fontSize: '14px' }}
padding={'4px 8px'}
onClick={() => setSendingWithSwap(true)}
>
+ Add a swap
</ButtonSecondary>
{account && (
<ButtonSecondary
style={{ fontSize: '14px', marginLeft: '8px' }}
padding={'4px 8px'}
width="fit-content"
disabled={atMaxAmountInput}
onClick={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
>
Input Max
</ButtonSecondary>
)}
</RowBetween>
</AutoColumn>
)}
<AutoColumn gap={'md'}>
{sendingWithSwap && (
<>
<CurrencyInputPanel
field={Field.INPUT}
label={independentField === Field.OUTPUT && parsedAmounts[Field.INPUT] ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
/>
{sendingWithSwap ? (
<ColumnCenter>
<RowBetween padding="0 1rem 0 12px">
<ArrowWrapper onClick={onSwitchTokens}>
<ArrowDown size="16" color={theme.text2} onClick={onSwitchTokens} />
</ArrowWrapper>
<ButtonSecondary
onClick={() => setSendingWithSwap(false)}
style={{ marginRight: '0px', width: 'fit-content', fontSize: '14px' }}
padding={'4px 6px'}
>
Remove Swap
</ButtonSecondary>
</RowBetween>
</ColumnCenter>
) : (
<CursorPointer>
<AutoColumn style={{ padding: '0 1rem' }}>
<ArrowWrapper>
<ArrowDown
size="16"
onClick={onSwitchTokens}
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
/>
</ArrowWrapper>
</AutoColumn>
</CursorPointer>
)}
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
// eslint-disable-next-line @typescript-eslint/no-empty-function
label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
id="swap-currency-output"
/>
{sendingWithSwap && (
<RowBetween padding="0 1rem 0 12px">
<ArrowDown size="16" color={theme.text2} />
</RowBetween>
)}
</>
)}
<AutoColumn gap="lg" justify="center">
<AddressInputPanel
onChange={_onRecipient}
onError={(error: boolean, input) => {
if (error && input !== '') {
setRecipientError('Invalid Recipient')
} else if (error && input === '') {
setRecipientError('Enter a Recipient')
} else {
setRecipientError(null)
}
}}
/>
</AutoColumn>
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
<Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
<RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}>
Price
</Text>
<Text
fontWeight={500}
fontSize={14}
color={theme.text2}
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
>
{bestTrade && showInverted
? (bestTrade?.executionPrice?.invert()?.toSignificant(6) ?? '') +
' ' +
tokens[Field.INPUT]?.symbol +
' per ' +
tokens[Field.OUTPUT]?.symbol
: (bestTrade?.executionPrice?.toSignificant(6) ?? '') +
' ' +
tokens[Field.OUTPUT]?.symbol +
' per ' +
tokens[Field.INPUT]?.symbol}
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
<Repeat size={14} />
</StyledBalanceMaxMini>
</Text>
</RowBetween>
{bestTrade && severity > 1 && (
<RowBetween>
<TYPE.main style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }} fontSize={14}>
Price Impact
</TYPE.main>
<RowFixed>
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
</RowFixed>
</RowBetween>
)}
</AutoColumn>
</Card>
)}
</AutoColumn>
<BottomGrouping>
{!account ? (
<ButtonLight
onClick={() => {
toggleWalletModal()
}}
>
Connect Wallet
</ButtonLight>
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
<Link
onClick={() => {
history.push('/add/' + tokens[Field.INPUT]?.address + '-' + tokens[Field.OUTPUT]?.address)
}}
>
Add liquidity now.
</Link>
</GreyCard>
) : mustApprove === true ? (
<ButtonLight onClick={approveCallback} disabled={pendingApprovalInput}>
{pendingApprovalInput ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonLight>
) : (
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
id="send-button"
disabled={(sendingWithSwap && !isSwapValid) || (!sendingWithSwap && !isSendValid)}
error={sendingWithSwap && isSwapValid && severity > 2}
>
<Text fontSize={20} fontWeight={500}>
{(sendingWithSwap ? swapError : null) ||
sendAmountError ||
recipientError ||
`Send${severity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
)}
<V1TradeLink bestV2Trade={bestTrade} />
</BottomGrouping>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
</Wrapper>
)
}

@ -1,7 +1,327 @@
import React from 'react'
import ExchangePage from '../../components/ExchangePage'
import { QueryParams } from '../../utils'
import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useContext, useState } from 'react'
import { ArrowDown, Repeat } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { ButtonError, ButtonLight } from '../../components/Button'
import Card, { GreyCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import QuestionHelper from '../../components/Question'
import { RowBetween, RowFixed } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import FormattedPriceImpact from '../../components/swap/FormattedPriceImpact'
import { ArrowWrapper, BottomGrouping, Dots, StyledBalanceMaxMini, Wrapper } from '../../components/swap/styleds'
import SwapModalFooter from '../../components/swap/SwapModalFooter'
import V1TradeLink from '../../components/swap/V1TradeLink'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
import { useWeb3React } from '../../hooks'
import { useApproveCallback } from '../../hooks/useApproveCallback'
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 { useHasPendingApproval } from '../../state/transactions/hooks'
import { CursorPointer, Link, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
import SwapModalHeader from '../../components/swap/SwapModalHeader'
export default function Swap({ params }: { params: QueryParams }) {
return <ExchangePage sendingInput={false} params={params} />
export default function Swap({ history, location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search)
// text translation
// const { t } = useTranslation()
const { chainId, account } = useWeb3React()
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
const { independentField, typedValue } = useSwapState()
const { bestTrade, tokenBalances, parsedAmounts, tokens, error } = useDerivedSwapInfo()
const isValid = !error
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirmed
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
const route = bestTrade?.route
const userHasSpecifiedInputOutput =
!!tokens[Field.INPUT] &&
!!tokens[Field.OUTPUT] &&
!!parsedAmounts[independentField] &&
parsedAmounts[independentField].greaterThan(JSBI.BigInt(0))
const noRoute = !route
// check whether the user has approved the router on the input token
const [mustApprove, approveCallback] = useApproveCallback(bestTrade, allowedSlippage)
const pendingApprovalInput = useHasPendingApproval(tokens[Field.INPUT]?.address)
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] &&
!!WETH[chainId] &&
tokenBalances[Field.INPUT].greaterThan(
new TokenAmount(tokens[Field.INPUT], tokens[Field.INPUT].equals(WETH[chainId]) ? MIN_ETH : '0')
)
? tokens[Field.INPUT].equals(WETH[chainId])
? tokenBalances[Field.INPUT].subtract(new TokenAmount(WETH[chainId], MIN_ETH))
: tokenBalances[Field.INPUT]
: undefined
const atMaxAmountInput: boolean =
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
// reset modal state when closed
function resetModal() {
// clear input if txn submitted
if (!pendingConfirmation) {
onUserInput(Field.INPUT, '')
}
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowAdvanced(false)
}
// the callback to execute the swap
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline)
function onSwap() {
setAttemptingTxn(true)
swapCallback().then(hash => {
setTxHash(hash)
setPendingConfirmation(false)
ReactGA.event({
category: 'Swap',
action: 'Swap w/o Send',
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join('/')
})
})
}
// errors
const [showInverted, setShowInverted] = useState<boolean>(false)
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(bestTrade)
// warnings on slippage
const priceImpactSeverity = warningServerity(priceImpactWithoutFee)
function modalHeader() {
return (
<SwapModalHeader
independentField={independentField}
priceImpactSeverity={priceImpactSeverity}
tokens={tokens}
formattedAmounts={formattedAmounts}
slippageAdjustedAmounts={slippageAdjustedAmounts}
/>
)
}
function modalBottom() {
return (
<SwapModalFooter
confirmText={priceImpactSeverity > 2 ? 'Swap Anyway' : 'Confirm Swap'}
showInverted={showInverted}
severity={priceImpactSeverity}
setShowInverted={setShowInverted}
onSwap={onSwap}
realizedLPFee={realizedLPFee}
parsedAmounts={parsedAmounts}
priceImpactWithoutFee={priceImpactWithoutFee}
slippageAdjustedAmounts={slippageAdjustedAmounts}
trade={bestTrade}
/>
)
}
// text to show while loading
const pendingText = `Swapping ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${
tokens[Field.INPUT]?.symbol
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
return (
<Wrapper id="swap-page">
<ConfirmationModal
isOpen={showConfirm}
title="Confirm Swap"
onDismiss={() => {
resetModal()
setShowConfirm(false)
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
/>
<AutoColumn gap={'md'}>
<>
<CurrencyInputPanel
field={Field.INPUT}
label={independentField === Field.OUTPUT ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
/>
<CursorPointer>
<AutoColumn style={{ padding: '0 1rem' }}>
<ArrowWrapper>
<ArrowDown
size="16"
onClick={onSwitchTokens}
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
/>
</ArrowWrapper>
</AutoColumn>
</CursorPointer>
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
// eslint-disable-next-line @typescript-eslint/no-empty-function
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
id="swap-currency-output"
/>
</>
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
<Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
<RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}>
Price
</Text>
<Text
fontWeight={500}
fontSize={14}
color={theme.text2}
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
>
{bestTrade && showInverted
? (bestTrade?.executionPrice?.invert()?.toSignificant(6) ?? '') +
' ' +
tokens[Field.INPUT]?.symbol +
' per ' +
tokens[Field.OUTPUT]?.symbol
: (bestTrade?.executionPrice?.toSignificant(6) ?? '') +
' ' +
tokens[Field.OUTPUT]?.symbol +
' per ' +
tokens[Field.INPUT]?.symbol}
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
<Repeat size={14} />
</StyledBalanceMaxMini>
</Text>
</RowBetween>
{bestTrade && priceImpactSeverity > 1 && (
<RowBetween>
<TYPE.main style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }} fontSize={14}>
Price Impact
</TYPE.main>
<RowFixed>
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
</RowFixed>
</RowBetween>
)}
</AutoColumn>
</Card>
)}
</AutoColumn>
<BottomGrouping>
{!account ? (
<ButtonLight
onClick={() => {
toggleWalletModal()
}}
>
Connect Wallet
</ButtonLight>
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
<Link
onClick={() => {
history.push('/add/' + tokens[Field.INPUT]?.address + '-' + tokens[Field.OUTPUT]?.address)
}}
>
{' '}
Add liquidity now.
</Link>
</GreyCard>
) : mustApprove === true ? (
<ButtonLight onClick={approveCallback} disabled={pendingApprovalInput}>
{pendingApprovalInput ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonLight>
) : (
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
id="swap-button"
disabled={!isValid}
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={20} fontWeight={500}>
{error ?? `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
)}
<V1TradeLink bestV2Trade={bestTrade} />
</BottomGrouping>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
</Wrapper>
)
}

@ -23,6 +23,5 @@ export type PopupContent =
export const updateBlockNumber = createAction<{ networkId: number; blockNumber: number | null }>('updateBlockNumber')
export const toggleWalletModal = createAction<void>('toggleWalletModal')
export const toggleUserAdvanced = createAction<void>('toggleUserAdvanced')
export const addPopup = createAction<{ content: PopupContent }>('addPopup')
export const removePopup = createAction<{ key: string }>('removePopup')

@ -19,10 +19,6 @@ export function useWalletModalToggle() {
return useCallback(() => dispatch(toggleWalletModal()), [dispatch])
}
export function useUserAdvanced() {
return useSelector((state: AppState) => state.application.userAdvanced)
}
// returns a function that allows adding a popup
export function useAddPopup(): (content: PopupContent) => void {
const dispatch = useDispatch()

@ -1,12 +1,5 @@
import { createReducer, nanoid } from '@reduxjs/toolkit'
import {
addPopup,
PopupContent,
removePopup,
toggleUserAdvanced,
toggleWalletModal,
updateBlockNumber
} from './actions'
import { addPopup, PopupContent, removePopup, toggleWalletModal, updateBlockNumber } from './actions'
type PopupList = Array<{ key: string; show: boolean; content: PopupContent }>
@ -14,14 +7,12 @@ interface ApplicationState {
blockNumber: { [chainId: number]: number }
popupList: PopupList
walletModalOpen: boolean
userAdvanced: boolean
}
const initialState: ApplicationState = {
blockNumber: {},
popupList: [],
walletModalOpen: false,
userAdvanced: false
walletModalOpen: false
}
export default createReducer(initialState, builder =>
@ -30,9 +21,6 @@ export default createReducer(initialState, builder =>
const { networkId, blockNumber } = action.payload
state.blockNumber[networkId] = blockNumber
})
.addCase(toggleUserAdvanced, state => {
state.userAdvanced = !state.userAdvanced
})
.addCase(toggleWalletModal, state => {
state.walletModalOpen = !state.walletModalOpen
})

@ -3,6 +3,7 @@ import application from './application/reducer'
import { updateVersion } from './user/actions'
import user from './user/reducer'
import wallet from './wallet/reducer'
import swap from './swap/reducer'
import transactions from './transactions/reducer'
import { save, load } from 'redux-localstorage-simple'
@ -13,7 +14,8 @@ const store = configureStore({
application,
user,
transactions,
wallet
wallet,
swap
},
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
preloadedState: load({ states: PERSISTED_KEYS })

11
src/state/swap/actions.ts Normal file

@ -0,0 +1,11 @@
import { createAction } from '@reduxjs/toolkit'
export enum Field {
INPUT = 'INPUT',
OUTPUT = 'OUTPUT'
}
export const setDefaultsFromURL = 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')

147
src/state/swap/hooks.ts Normal file

@ -0,0 +1,147 @@
import { parseUnits } from '@ethersproject/units'
import { JSBI, Token, TokenAmount, Trade } from '@uniswap/sdk'
import { useCallback, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useWeb3React } from '../../hooks'
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'
export function useSwapState(): AppState['swap'] {
return useSelector<AppState, AppState['swap']>(state => state.swap)
}
export function useSwapActionHandlers(): {
onTokenSelection: (field: Field, address: string) => void
onSwitchTokens: () => void
onUserInput: (field: Field, typedValue: string) => void
} {
const dispatch = useDispatch<AppDispatch>()
const onTokenSelection = useCallback(
(field: Field, address: string) => {
dispatch(
selectToken({
field,
address
})
)
},
[dispatch]
)
const onSwapTokens = useCallback(() => {
dispatch(switchTokens())
}, [dispatch])
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
dispatch(typeInput({ field, typedValue }))
},
[dispatch]
)
return {
onSwitchTokens: onSwapTokens,
onTokenSelection,
onUserInput
}
}
// try to parse a user entered amount for a given token
function tryParseAmount(value?: string, token?: Token): TokenAmount | undefined {
if (!value || !token) return
try {
const typedValueParsed = parseUnits(value, token.decimals).toString()
if (typedValueParsed !== '0') return new TokenAmount(token, JSBI.BigInt(typedValueParsed))
} catch (error) {
// should fail if the user specifies too many decimal places of precision (or maybe exceed max uint?)
console.debug(`Failed to parse input amount: "${value}"`, error)
}
}
// from the current swap inputs, compute the best trade and return it.
export function useDerivedSwapInfo(): {
tokens: { [field in Field]?: Token }
tokenBalances: { [field in Field]?: TokenAmount }
parsedAmounts: { [field in Field]?: TokenAmount }
bestTrade?: Trade
error?: string
} {
const { account } = useWeb3React()
const {
independentField,
typedValue,
[Field.INPUT]: { address: tokenInAddress },
[Field.OUTPUT]: { address: tokenOutAddress }
} = useSwapState()
const tokenIn = useTokenByAddressAndAutomaticallyAdd(tokenInAddress)
const tokenOut = useTokenByAddressAndAutomaticallyAdd(tokenOutAddress)
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account, [tokenIn, tokenOut])
const isExactIn: boolean = independentField === Field.INPUT
const amount = tryParseAmount(typedValue, isExactIn ? tokenIn : tokenOut)
const bestTradeExactIn = useTradeExactIn(isExactIn ? amount : null, tokenOut)
const bestTradeExactOut = useTradeExactOut(tokenIn, !isExactIn ? amount : null)
const bestTrade = isExactIn ? bestTradeExactIn : bestTradeExactOut
const parsedAmounts = {
[Field.INPUT]: isExactIn ? amount : bestTrade?.inputAmount,
[Field.OUTPUT]: isExactIn ? bestTrade?.outputAmount : amount
}
const tokenBalances = {
[Field.INPUT]: relevantTokenBalances?.[tokenIn?.address],
[Field.OUTPUT]: relevantTokenBalances?.[tokenOut?.address]
}
const tokens = {
[Field.INPUT]: tokenIn,
[Field.OUTPUT]: tokenOut
}
let error: string | undefined
if (!account) {
error = 'Connect Wallet'
}
if (!parsedAmounts[Field.INPUT]) {
error = error ?? 'Enter an amount'
}
if (!parsedAmounts[Field.OUTPUT]) {
error = error ?? 'Enter an amount'
}
if (
tokenBalances[Field.INPUT] &&
parsedAmounts[Field.INPUT] &&
tokenBalances[Field.INPUT].lessThan(parsedAmounts[Field.INPUT])
) {
error = 'Insufficient ' + tokens[Field.INPUT]?.symbol + ' balance'
}
return {
tokens,
tokenBalances,
parsedAmounts,
bestTrade,
error
}
}
// updates the swap state to use the defaults for a given network whenever the query
// string updates
export function useDefaultsFromURL(search?: string) {
const { chainId } = useWeb3React()
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
dispatch(setDefaultsFromURL({ chainId, queryString: search }))
}, [dispatch, search, chainId])
}

96
src/state/swap/reducer.ts Normal file

@ -0,0 +1,96 @@
import { parse } from 'qs'
import { createReducer } from '@reduxjs/toolkit'
import { WETH } from '@uniswap/sdk'
import { isAddress } from '../../utils'
import { Field, selectToken, setDefaultsFromURL, switchTokens, typeInput } from './actions'
export interface SwapState {
readonly independentField: Field
readonly typedValue: string
readonly [Field.INPUT]: {
readonly address: string | undefined
}
readonly [Field.OUTPUT]: {
readonly address: string | undefined
}
}
const initialState: SwapState = {
independentField: Field.INPUT,
typedValue: '',
[Field.INPUT]: {
address: ''
},
[Field.OUTPUT]: {
address: ''
}
}
function parseTokenURL(input: any, chainId: number): string {
if (typeof input !== 'string') return ''
const valid = isAddress(input)
if (valid) return valid
if (input.toLowerCase() === 'eth') return WETH[chainId]?.address ?? ''
return ''
}
export default createReducer<SwapState>(initialState, builder =>
builder
.addCase(setDefaultsFromURL, (state, { payload: { queryString, chainId } }) => {
if (queryString && queryString.length > 1) {
const result = parse(queryString.substr(1), { parseArrays: false })
const inToken = parseTokenURL(result.inputToken, chainId)
const outToken = parseTokenURL(result.outputToken, chainId)
return {
[Field.INPUT]: {
address: inToken
},
[Field.OUTPUT]: {
address: inToken === outToken ? '' : outToken
},
typedValue: typeof result.amount === 'string' ? result.amount : '',
independentField: result.exact === 'out' ? Field.OUTPUT : Field.INPUT
}
}
return {
...initialState,
[Field.INPUT]: {
address: WETH[chainId]?.address
}
}
})
.addCase(selectToken, (state, { payload: { address, field } }) => {
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 }
}
}
})
.addCase(switchTokens, state => {
return {
...state,
independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT,
[Field.INPUT]: { address: state[Field.OUTPUT].address },
[Field.OUTPUT]: { address: state[Field.INPUT].address }
}
})
.addCase(typeInput, (state, { payload: { field, typedValue } }) => {
return {
...state,
independentField: field,
typedValue
}
})
)

@ -107,7 +107,7 @@ export const Spinner = styled.img`
height: 16px;
`
export const Hover = styled.div`
export const CursorPointer = styled.div`
:hover {
cursor: pointer;
}

@ -54,38 +54,6 @@ export function getQueryParam(windowLocation: Location, name: string): string |
return q && q[1]
}
function parseUrlAddress(param: string): string {
const addr = isAddress(getQueryParam(window.location, param))
if (addr === false) {
return ''
}
return addr
}
function parseUrlTokenAmount(paramName: string): string {
const value = getQueryParam(window.location, paramName)
if (!isNaN(Number(value))) {
return ''
}
return value
}
export interface QueryParams {
readonly inputTokenAddress: string
readonly outputTokenAddress: string
readonly inputTokenAmount: string
readonly outputTokenAmount: string
}
export function getAllQueryParams(): QueryParams {
return {
inputTokenAddress: parseUrlAddress('inputTokenAddress'),
outputTokenAddress: parseUrlAddress('outputTokenAddress'),
inputTokenAmount: parseUrlTokenAmount('inputTokenAmount'),
outputTokenAmount: parseUrlTokenAmount('outputTokenAmount')
}
}
// shorten the checksummed version of the input address to have 0x + 4 characters at start and end
export function shortenAddress(address: string, chars = 4): string {
const parsed = isAddress(address)
@ -195,23 +163,6 @@ export async function getTokenDecimals(tokenAddress, library) {
})
}
// get the ether balance of an address
export async function getEtherBalance(address, library) {
if (!isAddress(address)) {
throw Error(`Invalid 'address' parameter '${address}'`)
}
return library.getBalance(address)
}
// get the token balance of an address
export async function getTokenBalance(tokenAddress, address, library) {
if (!isAddress(tokenAddress) || !isAddress(address)) {
throw Error(`Invalid 'tokenAddress' or 'address' parameter '${tokenAddress}' or '${address}'.`)
}
return getContract(tokenAddress, ERC20_ABI, library).balanceOf(address)
}
export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}

67
src/utils/prices.ts Normal file

@ -0,0 +1,67 @@
import { Fraction, JSBI, Percent, TokenAmount, Trade } from '@uniswap/sdk'
import { ALLOWED_SLIPPAGE_HIGH, ALLOWED_SLIPPAGE_LOW, ALLOWED_SLIPPAGE_MEDIUM } from '../constants'
import { Field } from '../state/swap/actions'
import { basisPointsToPercent } from './index'
const BASE_FEE = new Percent(JSBI.BigInt(30), JSBI.BigInt(10000))
const ONE_HUNDRED_PERCENT = new Percent(JSBI.BigInt(10000), JSBI.BigInt(10000))
const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(BASE_FEE)
// computes price breakdown for the trade
export function computeTradePriceBreakdown(
trade?: Trade
): { priceImpactWithoutFee?: Percent; realizedLPFee?: TokenAmount } {
// for each hop in our trade, take away the x*y=k price impact from 0.3% fees
// e.g. for 3 tokens/2 hops: 1 - ((1 - .03) * (1-.03))
const realizedLPFee = !trade
? undefined
: ONE_HUNDRED_PERCENT.subtract(
trade.route.pairs.reduce<Fraction>(
(currentFee: Fraction): Fraction => currentFee.multiply(INPUT_FRACTION_AFTER_FEE),
INPUT_FRACTION_AFTER_FEE
)
)
// remove lp fees from price impact
const priceImpactWithoutFeeFraction = trade?.slippage?.subtract(realizedLPFee)
// the x*y=k impact
const priceImpactWithoutFeePercent = priceImpactWithoutFeeFraction
? new Percent(priceImpactWithoutFeeFraction?.numerator, priceImpactWithoutFeeFraction?.denominator)
: undefined
// the amount of the input that accrues to LPs
const realizedLPFeeAmount =
realizedLPFee && new TokenAmount(trade.inputAmount.token, realizedLPFee.multiply(trade.inputAmount.raw).quotient)
return { priceImpactWithoutFee: priceImpactWithoutFeePercent, realizedLPFee: realizedLPFeeAmount }
}
// computes the minimum amount out and maximum amount in for a trade given a user specified allowed slippage in bips
export function computeSlippageAdjustedAmounts(
trade: Trade,
allowedSlippage: number
): { [field in Field]?: TokenAmount } {
const pct = basisPointsToPercent(allowedSlippage)
return {
[Field.INPUT]: trade?.maximumAmountIn(pct),
[Field.OUTPUT]: trade?.minimumAmountOut(pct)
}
}
export function warningServerity(priceImpact: Percent): 0 | 1 | 2 | 3 {
if (!priceImpact?.lessThan(ALLOWED_SLIPPAGE_HIGH)) return 3
if (!priceImpact?.lessThan(ALLOWED_SLIPPAGE_MEDIUM)) return 2
if (!priceImpact?.lessThan(ALLOWED_SLIPPAGE_LOW)) return 1
return 0
}
export function formatExecutionPrice(trade?: Trade, inverted?: boolean): string {
if (!trade) {
return ''
}
return inverted
? `${trade.executionPrice.invert().toSignificant(6)} ${trade.inputAmount.token.symbol} / ${
trade.outputAmount.token.symbol
}`
: `${trade.executionPrice.toSignificant(6)} ${trade.outputAmount.token.symbol} / ${trade.inputAmount.token.symbol}`
}

@ -2897,6 +2897,11 @@
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
"@types/qs@^6.9.2":
version "6.9.2"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.2.tgz#faab98ec4f96ee72c829b7ec0983af4f4d343113"
integrity sha512-a9bDi4Z3zCZf4Lv1X/vwnvbbDYSNz59h3i3KdyuYYN+YrLjSeJD0dnphdULDfySvUv6Exy/O0K6wX/kQpnPQ+A==
"@types/react-dom@^16.9.7":
version "16.9.7"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.7.tgz#60844d48ce252d7b2dccf0c7bb937130e27c0cd2"
@ -14307,6 +14312,11 @@ qs@6.7.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@^6.9.4:
version "6.9.4"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"