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:
parent
51e929bd1e
commit
095beae0c2
@ -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
|
||||
|
91
src/components/swap/AdvancedSwapDetails.tsx
Normal file
91
src/components/swap/AdvancedSwapDetails.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
47
src/components/swap/AdvancedSwapDetailsDropdown.tsx
Normal file
47
src/components/swap/AdvancedSwapDetailsDropdown.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
13
src/components/swap/FormattedPriceImpact.tsx
Normal file
13
src/components/swap/FormattedPriceImpact.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
30
src/components/swap/PriceSlippageWarningCard.tsx
Normal file
30
src/components/swap/PriceSlippageWarningCard.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
119
src/components/swap/SwapModalFooter.tsx
Normal file
119
src/components/swap/SwapModalFooter.tsx
Normal file
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
75
src/components/swap/SwapModalHeader.tsx
Normal file
75
src/components/swap/SwapModalHeader.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
55
src/components/swap/TransferModalHeader.tsx
Normal file
55
src/components/swap/TransferModalHeader.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
26
src/components/swap/V1TradeLink.tsx
Normal file
26
src/components/swap/V1TradeLink.tsx
Normal file
@ -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(() => {
|
||||
|
61
src/hooks/useApproveCallback.ts
Normal file
61
src/hooks/useApproveCallback.ts
Normal file
@ -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]
|
||||
}
|
64
src/hooks/useSendCallback.ts
Normal file
64
src/hooks/useSendCallback.ts
Normal file
@ -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])
|
||||
}
|
192
src/hooks/useSwapCallback.ts
Normal file
192
src/hooks/useSwapCallback.ts
Normal file
@ -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
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
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
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
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}`
|
||||
}
|
10
yarn.lock
10
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user