feat(send page): remove send page, implement recipient feature in swap page (#934)
* quick poc for removing swap page * accidental import * error for recipient field * query parameter working * undo id change * tweaks to match mocks better * tweaks to match mocks better * some extra integration tests * clean up nav tabs a bit * clean up nav tabs a bit * space swap/pool better * stop selecting button text when double clicking * remove unused transfer modal header * add info to swap confirm modal * shorten address * improve summary message, remove unused send callback, fix react ga event * fix lint errors * arrow color
This commit is contained in:
parent
8a845ee0e9
commit
21c1484c0e
@ -1,9 +1,7 @@
|
|||||||
describe('Send', () => {
|
describe('Send', () => {
|
||||||
beforeEach(() => cy.visit('/send'))
|
beforeEach(() => cy.visit('/send'))
|
||||||
|
|
||||||
it('can enter an amount into input', () => {
|
it('should redirect', () => {
|
||||||
cy.get('#sending-no-swap-input')
|
cy.url().should('include', '/swap')
|
||||||
.type('0.001', { delay: 200 })
|
|
||||||
.should('have.value', '0.001')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -40,4 +40,15 @@ describe('Swap', () => {
|
|||||||
cy.get('#swap-button').click()
|
cy.get('#swap-button').click()
|
||||||
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
|
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('add a recipient', () => {
|
||||||
|
cy.get('#add-recipient-button').click()
|
||||||
|
cy.get('#recipient').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('remove recipient', () => {
|
||||||
|
cy.get('#add-recipient-button').click()
|
||||||
|
cy.get('#remove-recipient-button').click()
|
||||||
|
cy.get('#recipient').should('not.exist')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { CheckCircle, Triangle, ExternalLink as LinkIcon } from 'react-feather'
|
import { CheckCircle, Triangle } from 'react-feather'
|
||||||
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
import { getEtherscanLink } from '../../utils'
|
import { getEtherscanLink } from '../../utils'
|
||||||
@ -50,8 +50,7 @@ export default function Transaction({ hash }: { hash: string }) {
|
|||||||
<TransactionWrapper>
|
<TransactionWrapper>
|
||||||
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
|
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
|
||||||
<RowFixed>
|
<RowFixed>
|
||||||
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText>
|
<TransactionStatusText>{summary ?? hash} ↗</TransactionStatusText>
|
||||||
<LinkIcon size={16} />
|
|
||||||
</RowFixed>
|
</RowFixed>
|
||||||
<IconWrapper pending={pending} success={success}>
|
<IconWrapper pending={pending} success={success}>
|
||||||
{pending ? <Loader /> : success ? <CheckCircle size="16" /> : <Triangle size="16" />}
|
{pending ? <Loader /> : success ? <CheckCircle size="16" /> : <Triangle size="16" />}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react'
|
import React, { useContext, useCallback } from 'react'
|
||||||
import styled, { ThemeContext } from 'styled-components'
|
import styled, { ThemeContext } from 'styled-components'
|
||||||
import useDebounce from '../../hooks/useDebounce'
|
import useENS from '../../hooks/useENS'
|
||||||
|
|
||||||
import { isAddress } from '../../utils'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
import { ExternalLink, TYPE } from '../../theme'
|
import { ExternalLink, TYPE } from '../../theme'
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
@ -24,6 +22,8 @@ const ContainerRow = styled.div<{ error: boolean }>`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 1.25rem;
|
border-radius: 1.25rem;
|
||||||
border: 1px solid ${({ error, theme }) => (error ? theme.red1 : theme.bg2)};
|
border: 1px solid ${({ error, theme }) => (error ? theme.red1 : theme.bg2)};
|
||||||
|
transition: border-color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')},
|
||||||
|
color 500ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||||
background-color: ${({ theme }) => theme.bg1};
|
background-color: ${({ theme }) => theme.bg1};
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -39,6 +39,7 @@ const Input = styled.input<{ error?: boolean }>`
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 0;
|
width: 0;
|
||||||
background-color: ${({ theme }) => theme.bg1};
|
background-color: ${({ theme }) => theme.bg1};
|
||||||
|
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||||
color: ${({ error, theme }) => (error ? theme.red1 : theme.primary1)};
|
color: ${({ error, theme }) => (error ? theme.red1 : theme.primary1)};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -64,120 +65,65 @@ const Input = styled.input<{ error?: boolean }>`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
interface Value {
|
||||||
|
address: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function AddressInputPanel({
|
export default function AddressInputPanel({
|
||||||
initialInput = '',
|
id,
|
||||||
onChange,
|
value,
|
||||||
onError
|
onChange
|
||||||
}: {
|
}: {
|
||||||
initialInput?: string
|
id?: string
|
||||||
onChange: (val: { address: string; name?: string }) => void
|
// the typed string value
|
||||||
onError: (error: boolean, input: string) => void
|
value: string
|
||||||
|
// triggers whenever the typed value changes
|
||||||
|
onChange: (value: string) => void
|
||||||
}) {
|
}) {
|
||||||
const { chainId, library } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
const [input, setInput] = useState(initialInput ? initialInput : '')
|
const { address, loading, name } = useENS(value)
|
||||||
const debouncedInput = useDebounce(input, 200)
|
|
||||||
|
|
||||||
const [data, setData] = useState<{ address: string; name: string }>({ address: undefined, name: undefined })
|
const handleInput = useCallback(
|
||||||
const [error, setError] = useState<boolean>(false)
|
event => {
|
||||||
|
|
||||||
// keep data and errors in sync
|
|
||||||
useEffect(() => {
|
|
||||||
onChange({ address: data.address, name: data.name })
|
|
||||||
}, [onChange, data.address, data.name])
|
|
||||||
useEffect(() => {
|
|
||||||
onError(error, input)
|
|
||||||
}, [onError, error, input])
|
|
||||||
|
|
||||||
// run parser on debounced input
|
|
||||||
useEffect(() => {
|
|
||||||
let stale = false
|
|
||||||
// if the input is an address, try to look up its name
|
|
||||||
if (isAddress(debouncedInput)) {
|
|
||||||
library
|
|
||||||
.lookupAddress(debouncedInput)
|
|
||||||
.then(name => {
|
|
||||||
if (stale) return
|
|
||||||
// if an ENS name exists, set it as the destination
|
|
||||||
if (name) {
|
|
||||||
setInput(name)
|
|
||||||
} else {
|
|
||||||
setData({ address: debouncedInput, name: '' })
|
|
||||||
setError(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (stale) return
|
|
||||||
setData({ address: debouncedInput, name: '' })
|
|
||||||
setError(null)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// otherwise try to look up the address of the input, treated as an ENS name
|
|
||||||
else {
|
|
||||||
if (debouncedInput !== '') {
|
|
||||||
library
|
|
||||||
.resolveName(debouncedInput)
|
|
||||||
.then(address => {
|
|
||||||
if (stale) return
|
|
||||||
// if the debounced input name resolves to an address
|
|
||||||
if (address) {
|
|
||||||
setData({ address: address, name: debouncedInput })
|
|
||||||
setError(null)
|
|
||||||
} else {
|
|
||||||
setError(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (stale) return
|
|
||||||
setError(true)
|
|
||||||
})
|
|
||||||
} else if (debouncedInput === '') {
|
|
||||||
setError(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stale = true
|
|
||||||
}
|
|
||||||
}, [debouncedInput, library])
|
|
||||||
|
|
||||||
function onInput(event) {
|
|
||||||
setData({ address: undefined, name: undefined })
|
|
||||||
setError(false)
|
|
||||||
const input = event.target.value
|
const input = event.target.value
|
||||||
const checksummedInput = isAddress(input.replace(/\s/g, '')) // delete whitespace
|
const withoutSpaces = input.replace(/\s+/g, '')
|
||||||
setInput(checksummedInput || input)
|
onChange(withoutSpaces)
|
||||||
}
|
},
|
||||||
|
[onChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
const error = Boolean(value.length > 0 && !loading && !address)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputPanel>
|
<InputPanel id={id}>
|
||||||
<ContainerRow error={input !== '' && error}>
|
<ContainerRow error={error}>
|
||||||
<InputContainer>
|
<InputContainer>
|
||||||
<AutoColumn gap="md">
|
<AutoColumn gap="md">
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
|
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
|
||||||
Recipient
|
Recipient
|
||||||
</TYPE.black>
|
</TYPE.black>
|
||||||
{data.address && (
|
{address && (
|
||||||
<ExternalLink
|
<ExternalLink href={getEtherscanLink(chainId, name ?? address, 'address')} style={{ fontSize: '14px' }}>
|
||||||
href={getEtherscanLink(chainId, data.name || data.address, 'address')}
|
|
||||||
style={{ fontSize: '14px' }}
|
|
||||||
>
|
|
||||||
(View on Etherscan)
|
(View on Etherscan)
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
)}
|
)}
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
<Input
|
<Input
|
||||||
|
className="recipient-address-input"
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
placeholder="Wallet Address or ENS name"
|
placeholder="Wallet Address or ENS name"
|
||||||
error={input !== '' && error}
|
error={error}
|
||||||
onChange={onInput}
|
pattern="^(0x[a-fA-F0-9]{40})$"
|
||||||
value={input}
|
onChange={handleInput}
|
||||||
|
value={value}
|
||||||
/>
|
/>
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
|
@ -1,37 +1,18 @@
|
|||||||
import React, { useCallback } from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { darken } from 'polished'
|
import { darken } from 'polished'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { withRouter, NavLink, Link as HistoryLink, RouteComponentProps } from 'react-router-dom'
|
import { NavLink, Link as HistoryLink } from 'react-router-dom'
|
||||||
import useBodyKeyDown from '../../hooks/useBodyKeyDown'
|
|
||||||
|
|
||||||
import { CursorPointer } from '../../theme'
|
|
||||||
import { ArrowLeft } from 'react-feather'
|
import { ArrowLeft } from 'react-feather'
|
||||||
import { RowBetween } from '../Row'
|
import { RowBetween } from '../Row'
|
||||||
import QuestionHelper from '../QuestionHelper'
|
import QuestionHelper from '../QuestionHelper'
|
||||||
|
|
||||||
const tabOrder = [
|
|
||||||
{
|
|
||||||
path: '/swap',
|
|
||||||
textKey: 'swap',
|
|
||||||
regex: /\/swap/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/send',
|
|
||||||
textKey: 'send',
|
|
||||||
regex: /\/send/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/pool',
|
|
||||||
textKey: 'pool',
|
|
||||||
regex: /\/pool/
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const Tabs = styled.div`
|
const Tabs = styled.div`
|
||||||
${({ theme }) => theme.flexRowNoWrap}
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 3rem;
|
border-radius: 3rem;
|
||||||
|
justify-content: space-evenly;
|
||||||
`
|
`
|
||||||
|
|
||||||
const activeClassName = 'ACTIVE'
|
const activeClassName = 'ACTIVE'
|
||||||
@ -43,7 +24,6 @@ const StyledNavLink = styled(NavLink).attrs({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
flex: 1 0 auto;
|
|
||||||
border-radius: 3rem;
|
border-radius: 3rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -68,43 +48,59 @@ const ActiveText = styled.div`
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ArrowLink = styled(ArrowLeft)`
|
const StyledArrowLeft = styled(ArrowLeft)`
|
||||||
color: ${({ theme }) => theme.text1};
|
color: ${({ theme }) => theme.text1};
|
||||||
`
|
`
|
||||||
|
|
||||||
function NavigationTabs({ location: { pathname }, history }: RouteComponentProps<{}>) {
|
export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const navigate = useCallback(
|
|
||||||
direction => {
|
|
||||||
const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex))
|
|
||||||
history.push(tabOrder[(tabIndex + tabOrder.length + direction) % tabOrder.length].path)
|
|
||||||
},
|
|
||||||
[pathname, history]
|
|
||||||
)
|
|
||||||
const navigateRight = useCallback(() => {
|
|
||||||
navigate(1)
|
|
||||||
}, [navigate])
|
|
||||||
const navigateLeft = useCallback(() => {
|
|
||||||
navigate(-1)
|
|
||||||
}, [navigate])
|
|
||||||
|
|
||||||
useBodyKeyDown('ArrowRight', navigateRight)
|
|
||||||
useBodyKeyDown('ArrowLeft', navigateLeft)
|
|
||||||
|
|
||||||
const adding = pathname.match('/add')
|
|
||||||
const removing = pathname.match('/remove')
|
|
||||||
const finding = pathname.match('/find')
|
|
||||||
const creating = pathname.match('/create')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Tabs style={{ marginBottom: '20px' }}>
|
||||||
{adding || removing ? (
|
<StyledNavLink id={`swap-nav-link`} to={'/swap'} isActive={() => active === 'swap'}>
|
||||||
|
{t('swap')}
|
||||||
|
</StyledNavLink>
|
||||||
|
<StyledNavLink id={`pool-nav-link`} to={'/pool'} isActive={() => active === 'pool'}>
|
||||||
|
{t('pool')}
|
||||||
|
</StyledNavLink>
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreatePoolTabs() {
|
||||||
|
return (
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<RowBetween style={{ padding: '1rem' }}>
|
<RowBetween style={{ padding: '1rem' }}>
|
||||||
<CursorPointer onClick={() => history.push('/pool')}>
|
<HistoryLink to="/pool">
|
||||||
<ArrowLink />
|
<StyledArrowLeft />
|
||||||
</CursorPointer>
|
</HistoryLink>
|
||||||
|
<ActiveText>Create Pool</ActiveText>
|
||||||
|
<QuestionHelper text={'Use this interface to create a new pool.'} />
|
||||||
|
</RowBetween>
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FindPoolTabs() {
|
||||||
|
return (
|
||||||
|
<Tabs>
|
||||||
|
<RowBetween style={{ padding: '1rem' }}>
|
||||||
|
<HistoryLink to="/pool">
|
||||||
|
<StyledArrowLeft />
|
||||||
|
</HistoryLink>
|
||||||
|
<ActiveText>Import Pool</ActiveText>
|
||||||
|
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
|
||||||
|
</RowBetween>
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddRemoveTabs({ adding }: { adding: boolean }) {
|
||||||
|
return (
|
||||||
|
<Tabs>
|
||||||
|
<RowBetween style={{ padding: '1rem' }}>
|
||||||
|
<HistoryLink to="/pool">
|
||||||
|
<StyledArrowLeft />
|
||||||
|
</HistoryLink>
|
||||||
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
|
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
|
||||||
<QuestionHelper
|
<QuestionHelper
|
||||||
text={
|
text={
|
||||||
@ -115,42 +111,5 @@ function NavigationTabs({ location: { pathname }, history }: RouteComponentProps
|
|||||||
/>
|
/>
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : finding ? (
|
|
||||||
<Tabs>
|
|
||||||
<RowBetween style={{ padding: '1rem' }}>
|
|
||||||
<HistoryLink to="/pool">
|
|
||||||
<ArrowLink />
|
|
||||||
</HistoryLink>
|
|
||||||
<ActiveText>Import Pool</ActiveText>
|
|
||||||
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
|
|
||||||
</RowBetween>
|
|
||||||
</Tabs>
|
|
||||||
) : creating ? (
|
|
||||||
<Tabs>
|
|
||||||
<RowBetween style={{ padding: '1rem' }}>
|
|
||||||
<HistoryLink to="/pool">
|
|
||||||
<ArrowLink />
|
|
||||||
</HistoryLink>
|
|
||||||
<ActiveText>Create Pool</ActiveText>
|
|
||||||
<QuestionHelper text={'Use this interface to create a new pool.'} />
|
|
||||||
</RowBetween>
|
|
||||||
</Tabs>
|
|
||||||
) : (
|
|
||||||
<Tabs style={{ marginBottom: '20px' }}>
|
|
||||||
{tabOrder.map(({ path, textKey, regex }) => (
|
|
||||||
<StyledNavLink
|
|
||||||
id={`${textKey}-nav-link`}
|
|
||||||
key={path}
|
|
||||||
to={path}
|
|
||||||
isActive={(_, { pathname }) => !!pathname.match(regex)}
|
|
||||||
>
|
|
||||||
{t(textKey)}
|
|
||||||
</StyledNavLink>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(NavigationTabs)
|
|
||||||
|
@ -62,9 +62,7 @@ export default function TxnPopup({
|
|||||||
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
|
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
|
||||||
</div>
|
</div>
|
||||||
<AutoColumn gap="8px">
|
<AutoColumn gap="8px">
|
||||||
<TYPE.body fontWeight={500}>
|
<TYPE.body fontWeight={500}>{summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
|
||||||
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
|
|
||||||
</TYPE.body>
|
|
||||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
<Fader count={count} />
|
<Fader count={count} />
|
||||||
|
@ -130,7 +130,7 @@ export default function Web3Status() {
|
|||||||
const { active, account, connector, error } = useWeb3React()
|
const { active, account, connector, error } = useWeb3React()
|
||||||
const contextNetwork = useWeb3React(NetworkContextName)
|
const contextNetwork = useWeb3React(NetworkContextName)
|
||||||
|
|
||||||
const ENSName = useENSName(account)
|
const { ENSName } = useENSName(account)
|
||||||
|
|
||||||
const allTransactions = useAllTransactions()
|
const allTransactions = useAllTransactions()
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { Text } from 'rebass'
|
|||||||
import { ThemeContext } from 'styled-components'
|
import { ThemeContext } from 'styled-components'
|
||||||
import { Field } from '../../state/swap/actions'
|
import { Field } from '../../state/swap/actions'
|
||||||
import { TYPE } from '../../theme'
|
import { TYPE } from '../../theme'
|
||||||
|
import { isAddress, shortenAddress } from '../../utils'
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
import { RowBetween, RowFixed } from '../Row'
|
import { RowBetween, RowFixed } from '../Row'
|
||||||
import TokenLogo from '../TokenLogo'
|
import TokenLogo from '../TokenLogo'
|
||||||
@ -15,13 +16,15 @@ export default function SwapModalHeader({
|
|||||||
formattedAmounts,
|
formattedAmounts,
|
||||||
slippageAdjustedAmounts,
|
slippageAdjustedAmounts,
|
||||||
priceImpactSeverity,
|
priceImpactSeverity,
|
||||||
independentField
|
independentField,
|
||||||
|
recipient
|
||||||
}: {
|
}: {
|
||||||
tokens: { [field in Field]?: Token }
|
tokens: { [field in Field]?: Token }
|
||||||
formattedAmounts: { [field in Field]?: string }
|
formattedAmounts: { [field in Field]?: string }
|
||||||
slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
|
slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
|
||||||
priceImpactSeverity: number
|
priceImpactSeverity: number
|
||||||
independentField: Field
|
independentField: Field
|
||||||
|
recipient: string | null
|
||||||
}) {
|
}) {
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
@ -71,6 +74,14 @@ export default function SwapModalHeader({
|
|||||||
</TYPE.italic>
|
</TYPE.italic>
|
||||||
)}
|
)}
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
|
{recipient !== null ? (
|
||||||
|
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '12px 0 0 0px' }}>
|
||||||
|
<TYPE.main>
|
||||||
|
Output will be sent to{' '}
|
||||||
|
<b title={recipient}>{isAddress(recipient) ? shortenAddress(recipient) : recipient}</b>
|
||||||
|
</TYPE.main>
|
||||||
|
</AutoColumn>
|
||||||
|
) : null}
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
import { TokenAmount } from '@uniswap/sdk'
|
|
||||||
import React from 'react'
|
|
||||||
import { Text } from 'rebass'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
|
||||||
import { ExternalLink, 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 } = useActiveWeb3React()
|
|
||||||
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">
|
|
||||||
<ExternalLink href={getEtherscanLink(chainId, ENSName, 'address')}>
|
|
||||||
<TYPE.blue fontSize={18}>
|
|
||||||
{recipient?.slice(0, 8)}...{recipient?.slice(34, 42)}↗
|
|
||||||
</TYPE.blue>
|
|
||||||
</ExternalLink>
|
|
||||||
<Copy toCopy={recipient} />
|
|
||||||
</AutoRow>
|
|
||||||
</AutoColumn>
|
|
||||||
) : (
|
|
||||||
<AutoRow gap="10px">
|
|
||||||
<ExternalLink href={getEtherscanLink(chainId, recipient, 'address')}>
|
|
||||||
<TYPE.blue fontSize={36}>
|
|
||||||
{recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}↗
|
|
||||||
</TYPE.blue>
|
|
||||||
</ExternalLink>
|
|
||||||
<Copy toCopy={recipient} />
|
|
||||||
</AutoRow>
|
|
||||||
)}
|
|
||||||
</AutoColumn>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
import styled from 'styled-components'
|
import styled, { css } from 'styled-components'
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
|
|
||||||
@ -8,18 +8,19 @@ export const Wrapper = styled.div`
|
|||||||
position: relative;
|
position: relative;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ArrowWrapper = styled.div`
|
export const ArrowWrapper = styled.div<{ clickable: boolean }>`
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
|
${({ clickable }) =>
|
||||||
|
clickable
|
||||||
|
? css`
|
||||||
:hover {
|
:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
: null}
|
||||||
|
`
|
||||||
|
|
||||||
export const SectionBreak = styled.div`
|
export const SectionBreak = styled.div`
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
21
src/hooks/useENS.ts
Normal file
21
src/hooks/useENS.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { isAddress } from '../utils'
|
||||||
|
import useENSAddress from './useENSAddress'
|
||||||
|
import useENSName from './useENSName'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a name or address, does a lookup to resolve to an address and name
|
||||||
|
* @param nameOrAddress ENS name or address
|
||||||
|
*/
|
||||||
|
export default function useENS(
|
||||||
|
nameOrAddress?: string | null
|
||||||
|
): { loading: boolean; address: string | null; name: string | null } {
|
||||||
|
const validated = isAddress(nameOrAddress)
|
||||||
|
const reverseLookup = useENSName(validated ? validated : undefined)
|
||||||
|
const lookup = useENSAddress(nameOrAddress)
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading: reverseLookup.loading || lookup.loading,
|
||||||
|
address: validated ? validated : lookup.address,
|
||||||
|
name: reverseLookup.ENSName ? reverseLookup.ENSName : !validated && lookup.address ? nameOrAddress || null : null
|
||||||
|
}
|
||||||
|
}
|
46
src/hooks/useENSAddress.ts
Normal file
46
src/hooks/useENSAddress.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useActiveWeb3React } from './index'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does a lookup for an ENS name to find its address.
|
||||||
|
*/
|
||||||
|
export default function useENSAddress(ensName?: string | null): { loading: boolean; address: string | null } {
|
||||||
|
const { library } = useActiveWeb3React()
|
||||||
|
|
||||||
|
const [address, setAddress] = useState<{ loading: boolean; address: string | null }>({
|
||||||
|
loading: false,
|
||||||
|
address: null
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!library || typeof ensName !== 'string') {
|
||||||
|
setAddress({ loading: false, address: null })
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
let stale = false
|
||||||
|
setAddress({ loading: true, address: null })
|
||||||
|
library
|
||||||
|
.resolveName(ensName)
|
||||||
|
.then(address => {
|
||||||
|
if (!stale) {
|
||||||
|
if (address) {
|
||||||
|
setAddress({ loading: false, address })
|
||||||
|
} else {
|
||||||
|
setAddress({ loading: false, address: null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!stale) {
|
||||||
|
setAddress({ loading: false, address: null })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stale = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [library, ensName])
|
||||||
|
|
||||||
|
return address
|
||||||
|
}
|
@ -6,39 +6,43 @@ import { useActiveWeb3React } from './index'
|
|||||||
* Does a reverse lookup for an address to find its ENS name.
|
* Does a reverse lookup for an address to find its ENS name.
|
||||||
* Note this is not the same as looking up an ENS name to find an address.
|
* Note this is not the same as looking up an ENS name to find an address.
|
||||||
*/
|
*/
|
||||||
export default function useENSName(address?: string): string | null {
|
export default function useENSName(address?: string): { ENSName: string | null; loading: boolean } {
|
||||||
const { library } = useActiveWeb3React()
|
const { library } = useActiveWeb3React()
|
||||||
|
|
||||||
const [ENSName, setENSName] = useState<string | null>(null)
|
const [ENSName, setENSName] = useState<{ ENSName: string | null; loading: boolean }>({
|
||||||
|
loading: false,
|
||||||
|
ENSName: null
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!library || !address) return
|
|
||||||
const validated = isAddress(address)
|
const validated = isAddress(address)
|
||||||
if (validated) {
|
if (!library || !validated) {
|
||||||
|
setENSName({ loading: false, ENSName: null })
|
||||||
|
return
|
||||||
|
} else {
|
||||||
let stale = false
|
let stale = false
|
||||||
|
setENSName({ loading: true, ENSName: null })
|
||||||
library
|
library
|
||||||
.lookupAddress(validated)
|
.lookupAddress(validated)
|
||||||
.then(name => {
|
.then(name => {
|
||||||
if (!stale) {
|
if (!stale) {
|
||||||
if (name) {
|
if (name) {
|
||||||
setENSName(name)
|
setENSName({ loading: false, ENSName: name })
|
||||||
} else {
|
} else {
|
||||||
setENSName(null)
|
setENSName({ loading: false, ENSName: null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!stale) {
|
if (!stale) {
|
||||||
setENSName(null)
|
setENSName({ loading: false, ENSName: null })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stale = true
|
stale = true
|
||||||
setENSName(null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}, [library, address])
|
}, [library, address])
|
||||||
|
|
||||||
return ENSName
|
return ENSName
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
import { BigNumber } from '@ethersproject/bignumber'
|
|
||||||
import { TransactionResponse } from '@ethersproject/providers'
|
|
||||||
import { WETH, TokenAmount, JSBI, ChainId } 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 { useTokenContract } from './useContract'
|
|
||||||
import { useActiveWeb3React } from './index'
|
|
||||||
import useENSName from './useENSName'
|
|
||||||
|
|
||||||
// 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 } = useActiveWeb3React()
|
|
||||||
const addTransaction = useTransactionAdder()
|
|
||||||
const ensName = useENSName(recipient)
|
|
||||||
const tokenContract = useTokenContract(amount?.token?.address)
|
|
||||||
const balance = useTokenBalanceTreatingWETHasETH(account ?? undefined, 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 (!chainId || !library || !account || !tokenContract) {
|
|
||||||
throw new Error('missing dependencies in onSend callback')
|
|
||||||
}
|
|
||||||
if (token.equals(WETH[chainId as ChainId])) {
|
|
||||||
return getSigner(library, account)
|
|
||||||
.sendTransaction({ to: recipient, value: BigNumber.from(amount.raw.toString()) })
|
|
||||||
.then((response: TransactionResponse) => {
|
|
||||||
addTransaction(response, {
|
|
||||||
summary: 'Send ' + amount.toSignificant(3) + ' ' + token?.symbol + ' to ' + (ensName ?? recipient)
|
|
||||||
})
|
|
||||||
return response.hash
|
|
||||||
})
|
|
||||||
.catch((error: 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: TransactionResponse) => {
|
|
||||||
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])
|
|
||||||
}
|
|
@ -1,18 +1,18 @@
|
|||||||
import { BigNumber } from '@ethersproject/bignumber'
|
import { BigNumber } from '@ethersproject/bignumber'
|
||||||
import { MaxUint256 } from '@ethersproject/constants'
|
import { MaxUint256 } from '@ethersproject/constants'
|
||||||
import { Contract } from '@ethersproject/contracts'
|
import { Contract } from '@ethersproject/contracts'
|
||||||
import { ChainId, Trade, TradeType, WETH } from '@uniswap/sdk'
|
import { Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
|
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
|
||||||
import { useTokenAllowance } from '../data/Allowances'
|
import { useTokenAllowance } from '../data/Allowances'
|
||||||
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||||
import { Field } from '../state/swap/actions'
|
import { Field } from '../state/swap/actions'
|
||||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||||
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
|
import { calculateGasMargin, getRouterContract, shortenAddress, isAddress } from '../utils'
|
||||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||||
import { useActiveWeb3React } from './index'
|
import { useActiveWeb3React } from './index'
|
||||||
import { useV1ExchangeContract } from './useContract'
|
import { useV1ExchangeContract } from './useContract'
|
||||||
import useENSName from './useENSName'
|
import useENS from './useENS'
|
||||||
import { Version } from './useToggledVersion'
|
import { Version } from './useToggledVersion'
|
||||||
|
|
||||||
enum SwapType {
|
enum SwapType {
|
||||||
@ -59,15 +59,16 @@ function getSwapType(trade: Trade | undefined): SwapType | undefined {
|
|||||||
// returns a function that will execute a swap, if the parameters are all valid
|
// 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
|
// and the user has approved the slippage adjusted input amount for the trade
|
||||||
export function useSwapCallback(
|
export function useSwapCallback(
|
||||||
trade?: Trade, // trade to execute, required
|
trade: Trade | undefined, // trade to execute, required
|
||||||
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
|
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
|
||||||
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
|
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
|
||||||
to?: string // recipient of output, optional
|
recipientAddressOrName: string // the ENS name or address of the recipient of the trade
|
||||||
): null | (() => Promise<string>) {
|
): null | (() => Promise<string>) {
|
||||||
const { account, chainId, library } = useActiveWeb3React()
|
const { account, chainId, library } = useActiveWeb3React()
|
||||||
const addTransaction = useTransactionAdder()
|
const addTransaction = useTransactionAdder()
|
||||||
const recipient = to ? isAddress(to) : account
|
|
||||||
const ensName = useENSName(to)
|
const { address: recipient } = useENS(recipientAddressOrName)
|
||||||
|
|
||||||
const tradeVersion = getTradeVersion(trade)
|
const tradeVersion = getTradeVersion(trade)
|
||||||
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
|
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
|
||||||
const inputAllowance = useTokenAllowance(
|
const inputAllowance = useTokenAllowance(
|
||||||
@ -77,7 +78,7 @@ export function useSwapCallback(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (!trade || !recipient || !tradeVersion) return null
|
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null
|
||||||
|
|
||||||
// will always be defined
|
// will always be defined
|
||||||
const {
|
const {
|
||||||
@ -89,17 +90,13 @@ export function useSwapCallback(
|
|||||||
|
|
||||||
// no allowance
|
// no allowance
|
||||||
if (
|
if (
|
||||||
!trade.inputAmount.token.equals(WETH[chainId as ChainId]) &&
|
!trade.inputAmount.token.equals(WETH[chainId]) &&
|
||||||
(!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance))
|
(!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance))
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return async function onSwap() {
|
return async function onSwap() {
|
||||||
if (!chainId || !library || !account) {
|
|
||||||
throw new Error('missing dependencies in onSwap callback')
|
|
||||||
}
|
|
||||||
|
|
||||||
const contract: Contract | null =
|
const contract: Contract | null =
|
||||||
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
|
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
|
||||||
if (!contract) {
|
if (!contract) {
|
||||||
@ -283,7 +280,12 @@ export function useSwapCallback(
|
|||||||
const outputAmount = slippageAdjustedOutput.toSignificant(3)
|
const outputAmount = slippageAdjustedOutput.toSignificant(3)
|
||||||
|
|
||||||
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
|
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
|
||||||
const withRecipient = recipient === account ? base : `${base} to ${ensName ?? recipient}`
|
const withRecipient =
|
||||||
|
recipient === account
|
||||||
|
? base
|
||||||
|
: `${base} to ${
|
||||||
|
isAddress(recipientAddressOrName) ? shortenAddress(recipientAddressOrName) : recipientAddressOrName
|
||||||
|
}`
|
||||||
|
|
||||||
const withVersion =
|
const withVersion =
|
||||||
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
|
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
|
||||||
@ -310,15 +312,15 @@ export function useSwapCallback(
|
|||||||
}, [
|
}, [
|
||||||
trade,
|
trade,
|
||||||
recipient,
|
recipient,
|
||||||
tradeVersion,
|
|
||||||
allowedSlippage,
|
|
||||||
chainId,
|
|
||||||
inputAllowance,
|
|
||||||
library,
|
library,
|
||||||
account,
|
account,
|
||||||
|
tradeVersion,
|
||||||
|
chainId,
|
||||||
|
allowedSlippage,
|
||||||
|
inputAllowance,
|
||||||
v1Exchange,
|
v1Exchange,
|
||||||
deadline,
|
deadline,
|
||||||
addTransaction,
|
recipientAddressOrName,
|
||||||
ensName
|
addTransaction
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import { Field } from '../../state/mint/actions'
|
|||||||
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
|
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
|
||||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||||
import { useUserSlippageTolerance, useUserDeadline, useIsExpertMode } from '../../state/user/hooks'
|
import { useUserSlippageTolerance, useUserDeadline, useIsExpertMode } from '../../state/user/hooks'
|
||||||
|
import { AddRemoveTabs } from '../../components/NavigationTabs'
|
||||||
|
|
||||||
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
|
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
|
||||||
useDefaultsFromURLMatchParams(params)
|
useDefaultsFromURLMatchParams(params)
|
||||||
@ -307,6 +308,7 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppBody>
|
<AppBody>
|
||||||
|
<AddRemoveTabs adding={true} />
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={showConfirm}
|
isOpen={showConfirm}
|
||||||
|
@ -14,7 +14,6 @@ import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange'
|
|||||||
import Pool from './Pool'
|
import Pool from './Pool'
|
||||||
import PoolFinder from './PoolFinder'
|
import PoolFinder from './PoolFinder'
|
||||||
import RemoveLiquidity from './RemoveLiquidity'
|
import RemoveLiquidity from './RemoveLiquidity'
|
||||||
import Send from './Send'
|
|
||||||
import Swap from './Swap'
|
import Swap from './Swap'
|
||||||
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
|
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
|
||||||
|
|
||||||
@ -69,7 +68,7 @@ export default function App() {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Route exact strict path="/swap" component={Swap} />
|
<Route exact strict path="/swap" component={Swap} />
|
||||||
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
|
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
|
||||||
<Route exact strict path="/send" component={Send} />
|
<Route exact strict path="/send" component={RedirectPathToSwapOnly} />
|
||||||
<Route exact strict path="/find" component={PoolFinder} />
|
<Route exact strict path="/find" component={PoolFinder} />
|
||||||
<Route exact strict path="/pool" component={Pool} />
|
<Route exact strict path="/pool" component={Pool} />
|
||||||
<Route exact strict path="/create" component={CreatePool} />
|
<Route exact strict path="/create" component={CreatePool} />
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import NavigationTabs from '../components/NavigationTabs'
|
|
||||||
|
|
||||||
export const BodyWrapper = styled.div`
|
export const BodyWrapper = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -17,10 +16,5 @@ export const BodyWrapper = styled.div`
|
|||||||
* The styled container element that wraps the content of most pages and the tabs.
|
* The styled container element that wraps the content of most pages and the tabs.
|
||||||
*/
|
*/
|
||||||
export default function AppBody({ children }: { children: React.ReactNode }) {
|
export default function AppBody({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return <BodyWrapper>{children}</BodyWrapper>
|
||||||
<BodyWrapper>
|
|
||||||
<NavigationTabs />
|
|
||||||
<>{children}</>
|
|
||||||
</BodyWrapper>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { RouteComponentProps, Redirect } from 'react-router-dom'
|
import { RouteComponentProps, Redirect } from 'react-router-dom'
|
||||||
import { Token, WETH } from '@uniswap/sdk'
|
import { Token, WETH } from '@uniswap/sdk'
|
||||||
|
import { CreatePoolTabs } from '../../components/NavigationTabs'
|
||||||
import AppBody from '../AppBody'
|
import AppBody from '../AppBody'
|
||||||
|
|
||||||
import Row, { AutoRow } from '../../components/Row'
|
import Row, { AutoRow } from '../../components/Row'
|
||||||
@ -56,6 +57,7 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBody>
|
<AppBody>
|
||||||
|
<CreatePoolTabs />
|
||||||
<AutoColumn gap="20px">
|
<AutoColumn gap="20px">
|
||||||
<AutoColumn gap="24px">
|
<AutoColumn gap="24px">
|
||||||
{!token0Address ? (
|
{!token0Address ? (
|
||||||
|
@ -2,6 +2,7 @@ import React, { useState, useContext, useCallback } from 'react'
|
|||||||
import styled, { ThemeContext } from 'styled-components'
|
import styled, { ThemeContext } from 'styled-components'
|
||||||
import { JSBI } from '@uniswap/sdk'
|
import { JSBI } from '@uniswap/sdk'
|
||||||
import { RouteComponentProps } from 'react-router-dom'
|
import { RouteComponentProps } from 'react-router-dom'
|
||||||
|
import { SwapPoolTabs } from '../../components/NavigationTabs'
|
||||||
|
|
||||||
import Question from '../../components/QuestionHelper'
|
import Question from '../../components/QuestionHelper'
|
||||||
import PairSearchModal from '../../components/SearchModal/PairSearchModal'
|
import PairSearchModal from '../../components/SearchModal/PairSearchModal'
|
||||||
@ -70,6 +71,7 @@ export default function Pool({ history }: RouteComponentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBody>
|
<AppBody>
|
||||||
|
<SwapPoolTabs active={'pool'} />
|
||||||
<AutoColumn gap="lg" justify="center">
|
<AutoColumn gap="lg" justify="center">
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
id="join-pool-button"
|
id="join-pool-button"
|
||||||
|
@ -5,6 +5,7 @@ import { Text } from 'rebass'
|
|||||||
import { ButtonDropdownLight } from '../../components/Button'
|
import { ButtonDropdownLight } from '../../components/Button'
|
||||||
import { LightCard } from '../../components/Card'
|
import { LightCard } from '../../components/Card'
|
||||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||||
|
import { FindPoolTabs } from '../../components/NavigationTabs'
|
||||||
import PositionCard from '../../components/PositionCard'
|
import PositionCard from '../../components/PositionCard'
|
||||||
import Row from '../../components/Row'
|
import Row from '../../components/Row'
|
||||||
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
|
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
|
||||||
@ -61,6 +62,7 @@ export default function PoolFinder() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBody>
|
<AppBody>
|
||||||
|
<FindPoolTabs />
|
||||||
<AutoColumn gap="md">
|
<AutoColumn gap="md">
|
||||||
<ButtonDropdownLight
|
<ButtonDropdownLight
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -13,6 +13,7 @@ import { AutoColumn, ColumnCenter } from '../../components/Column'
|
|||||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||||
import DoubleLogo from '../../components/DoubleLogo'
|
import DoubleLogo from '../../components/DoubleLogo'
|
||||||
|
import { AddRemoveTabs } from '../../components/NavigationTabs'
|
||||||
import PositionCard from '../../components/PositionCard'
|
import PositionCard from '../../components/PositionCard'
|
||||||
import Row, { RowBetween, RowFixed } from '../../components/Row'
|
import Row, { RowBetween, RowFixed } from '../../components/Row'
|
||||||
|
|
||||||
@ -390,6 +391,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppBody>
|
<AppBody>
|
||||||
|
<AddRemoveTabs adding={false} />
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={showConfirm}
|
isOpen={showConfirm}
|
||||||
|
@ -1,592 +0,0 @@
|
|||||||
import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
|
|
||||||
import React, { useContext, useEffect, useState } from 'react'
|
|
||||||
import { ArrowDown } from 'react-feather'
|
|
||||||
import ReactGA from 'react-ga'
|
|
||||||
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 { AutoRow, RowBetween } from '../../components/Row'
|
|
||||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
|
||||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
|
||||||
import SwapModalFooter from '../../components/swap/SwapModalFooter'
|
|
||||||
import { ArrowWrapper, BottomGrouping, Dots, InputGroup, StyledNumerical, Wrapper } from '../../components/swap/styleds'
|
|
||||||
import TradePrice from '../../components/swap/TradePrice'
|
|
||||||
import { TransferModalHeader } from '../../components/swap/TransferModalHeader'
|
|
||||||
import BetterTradeLink from '../../components/swap/BetterTradeLink'
|
|
||||||
import TokenLogo from '../../components/TokenLogo'
|
|
||||||
import { TokenWarningCards } from '../../components/TokenWarningCard'
|
|
||||||
import { INITIAL_ALLOWED_SLIPPAGE, MIN_ETH, BETTER_TRADE_LINK_THRESHOLD } from '../../constants'
|
|
||||||
import { getTradeVersion, isTradeBetter } from '../../data/V1'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
|
||||||
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
|
|
||||||
import { useSendCallback } from '../../hooks/useSendCallback'
|
|
||||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
|
||||||
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
|
|
||||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
|
||||||
import { Field } from '../../state/swap/actions'
|
|
||||||
import {
|
|
||||||
useDefaultsFromURLSearch,
|
|
||||||
useDerivedSwapInfo,
|
|
||||||
useSwapActionHandlers,
|
|
||||||
useSwapState
|
|
||||||
} from '../../state/swap/hooks'
|
|
||||||
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
|
||||||
import { CursorPointer, TYPE } from '../../theme'
|
|
||||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
|
||||||
import AppBody from '../AppBody'
|
|
||||||
import { useUserSlippageTolerance, useUserDeadline, useExpertModeManager } from '../../state/user/hooks'
|
|
||||||
import { ClickableText } from '../Pool/styleds'
|
|
||||||
|
|
||||||
export default function Send() {
|
|
||||||
// override auto ETH populate to allow for single inputs or swap and send
|
|
||||||
useDefaultsFromURLSearch(true)
|
|
||||||
|
|
||||||
// text translation
|
|
||||||
// const { t } = useTranslation()
|
|
||||||
const { chainId, account } = useActiveWeb3React()
|
|
||||||
const theme = useContext(ThemeContext)
|
|
||||||
|
|
||||||
// toggle wallet when disconnected
|
|
||||||
const toggleWalletModal = useWalletModalToggle()
|
|
||||||
|
|
||||||
// for expert mode
|
|
||||||
const toggleSettings = useToggleSettingsMenu()
|
|
||||||
const [expertMode] = useExpertModeManager()
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
[Field.OUTPUT]: { address: output }
|
|
||||||
} = useSwapState()
|
|
||||||
|
|
||||||
// if output is valid set to sending view (will reset to undefined on remove swap)
|
|
||||||
useEffect(() => {
|
|
||||||
if (output) {
|
|
||||||
setSendingWithSwap(true)
|
|
||||||
}
|
|
||||||
}, [output])
|
|
||||||
|
|
||||||
const {
|
|
||||||
parsedAmount,
|
|
||||||
bestTrade: bestTradeV2,
|
|
||||||
tokenBalances,
|
|
||||||
tokens,
|
|
||||||
error: swapError,
|
|
||||||
v1Trade
|
|
||||||
} = useDerivedSwapInfo()
|
|
||||||
|
|
||||||
const toggledVersion = useToggledVersion()
|
|
||||||
const bestTrade = {
|
|
||||||
[Version.v1]: v1Trade,
|
|
||||||
[Version.v2]: bestTradeV2
|
|
||||||
}[toggledVersion]
|
|
||||||
|
|
||||||
const betterTradeLinkVersion: Version | undefined =
|
|
||||||
toggledVersion === Version.v2 && isTradeBetter(bestTradeV2, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
|
|
||||||
? Version.v1
|
|
||||||
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, bestTradeV2)
|
|
||||||
? Version.v2
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const parsedAmounts = {
|
|
||||||
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
|
|
||||||
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSwapValid = !swapError && !recipientError && bestTrade
|
|
||||||
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
|
||||||
|
|
||||||
// modal and loading
|
|
||||||
const [showConfirm, setShowConfirm] = useState<boolean>(false) // show confirmation modal
|
|
||||||
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
|
|
||||||
const [txHash, setTxHash] = useState<string>('')
|
|
||||||
const [deadline] = useUserDeadline() // custom from user settings
|
|
||||||
const [allowedSlippage] = useUserSlippageTolerance() // custom from user settings
|
|
||||||
|
|
||||||
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 [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
|
|
||||||
|
|
||||||
// check if user has gone through approval process, used to show two step buttons, reset on token change
|
|
||||||
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
|
|
||||||
|
|
||||||
// mark when a user has submitted an approval, reset onTokenSelection for input field
|
|
||||||
useEffect(() => {
|
|
||||||
if (approval === ApprovalState.PENDING) {
|
|
||||||
setApprovalSubmitted(true)
|
|
||||||
}
|
|
||||||
}, [approval, approvalSubmitted])
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
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 swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline, recipient)
|
|
||||||
|
|
||||||
function onSwap() {
|
|
||||||
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setAttemptingTxn(true)
|
|
||||||
swapCallback()
|
|
||||||
.then(hash => {
|
|
||||||
setAttemptingTxn(false)
|
|
||||||
setTxHash(hash)
|
|
||||||
|
|
||||||
ReactGA.event({
|
|
||||||
category: 'Send',
|
|
||||||
action: recipient === account ? 'Swap w/o Send' : 'Swap w/ Send',
|
|
||||||
label: [
|
|
||||||
bestTrade.inputAmount.token.symbol,
|
|
||||||
bestTrade.outputAmount.token.symbol,
|
|
||||||
getTradeVersion(bestTrade)
|
|
||||||
].join('/')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
setAttemptingTxn(false)
|
|
||||||
// we only care if the error is something _other_ than the user rejected the tx
|
|
||||||
if (error?.code !== 4001) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendCallback = useSendCallback(parsedAmounts?.[Field.INPUT], recipient)
|
|
||||||
const isSendValid = sendCallback !== null && (sendingWithSwap === false || approval === ApprovalState.APPROVED)
|
|
||||||
|
|
||||||
async function onSend() {
|
|
||||||
setAttemptingTxn(true)
|
|
||||||
sendCallback()
|
|
||||||
.then(hash => {
|
|
||||||
setAttemptingTxn(false)
|
|
||||||
setTxHash(hash)
|
|
||||||
|
|
||||||
ReactGA.event({ category: 'Send', action: 'Send', label: tokens[Field.INPUT]?.symbol })
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
setAttemptingTxn(false)
|
|
||||||
// we only care if the error is something _other_ than the user rejected the tx
|
|
||||||
if (error?.code !== 4001) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
|
||||||
|
|
||||||
// warnings on slippage
|
|
||||||
const severity = !sendingWithSwap ? 0 : warningSeverity(priceImpactWithoutFee)
|
|
||||||
|
|
||||||
// show approval buttons when: no errors on input, not approved or pending, or has been approved in this session
|
|
||||||
const showApproveFlow =
|
|
||||||
((sendingWithSwap && isSwapValid) || (!sendingWithSwap && isSendValid)) &&
|
|
||||||
(approval === ApprovalState.NOT_APPROVED ||
|
|
||||||
approval === ApprovalState.PENDING ||
|
|
||||||
(approvalSubmitted && approval === ApprovalState.APPROVED)) &&
|
|
||||||
!(severity > 3 && !expertMode)
|
|
||||||
|
|
||||||
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?.[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 (
|
|
||||||
<>
|
|
||||||
{sendingWithSwap ? <TokenWarningCards tokens={tokens} /> : null}
|
|
||||||
<AppBody>
|
|
||||||
<Wrapper id="send-page">
|
|
||||||
<ConfirmationModal
|
|
||||||
isOpen={showConfirm}
|
|
||||||
title={sendingWithSwap ? 'Confirm swap and send' : 'Confirm Send'}
|
|
||||||
onDismiss={() => {
|
|
||||||
setShowConfirm(false)
|
|
||||||
if (txHash) {
|
|
||||||
onUserInput(Field.INPUT, '')
|
|
||||||
}
|
|
||||||
setTxHash('')
|
|
||||||
}}
|
|
||||||
attemptingTxn={attemptingTxn}
|
|
||||||
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 => {
|
|
||||||
setApprovalSubmitted(false)
|
|
||||||
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)
|
|
||||||
onTokenSelection(Field.OUTPUT, null)
|
|
||||||
}}
|
|
||||||
style={{ marginRight: '0px', width: 'auto', 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}
|
|
||||||
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>
|
|
||||||
{sendingWithSwap && (
|
|
||||||
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
|
|
||||||
<AutoColumn gap="4px">
|
|
||||||
<RowBetween align="center">
|
|
||||||
<Text fontWeight={500} fontSize={14} color={theme.text2}>
|
|
||||||
Price
|
|
||||||
</Text>
|
|
||||||
<TradePrice
|
|
||||||
inputToken={tokens[Field.INPUT]}
|
|
||||||
outputToken={tokens[Field.OUTPUT]}
|
|
||||||
price={bestTrade?.executionPrice}
|
|
||||||
showInverted={showInverted}
|
|
||||||
setShowInverted={setShowInverted}
|
|
||||||
/>
|
|
||||||
</RowBetween>
|
|
||||||
|
|
||||||
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
|
|
||||||
<RowBetween align="center">
|
|
||||||
<ClickableText>
|
|
||||||
<Text fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
|
|
||||||
Slippage Tolerance
|
|
||||||
</Text>
|
|
||||||
</ClickableText>
|
|
||||||
<ClickableText>
|
|
||||||
<Text fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
|
|
||||||
{allowedSlippage ? allowedSlippage / 100 : '-'}%
|
|
||||||
</Text>
|
|
||||||
</ClickableText>
|
|
||||||
</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>
|
|
||||||
</GreyCard>
|
|
||||||
) : showApproveFlow ? (
|
|
||||||
<RowBetween>
|
|
||||||
<ButtonPrimary
|
|
||||||
onClick={approveCallback}
|
|
||||||
disabled={approval !== ApprovalState.NOT_APPROVED || approvalSubmitted}
|
|
||||||
width="48%"
|
|
||||||
altDisbaledStyle={approval === ApprovalState.PENDING} // show solid button while waiting
|
|
||||||
>
|
|
||||||
{approval === ApprovalState.PENDING ? (
|
|
||||||
<Dots>Approving</Dots>
|
|
||||||
) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
|
|
||||||
'Approved'
|
|
||||||
) : (
|
|
||||||
'Approve ' + tokens[Field.INPUT]?.symbol
|
|
||||||
)}
|
|
||||||
</ButtonPrimary>
|
|
||||||
<ButtonError
|
|
||||||
onClick={() => {
|
|
||||||
expertMode ? (sendingWithSwap ? onSwap() : onSend()) : setShowConfirm(true)
|
|
||||||
}}
|
|
||||||
width="48%"
|
|
||||||
id="send-button"
|
|
||||||
disabled={approval !== ApprovalState.APPROVED}
|
|
||||||
error={sendingWithSwap && isSwapValid && severity > 2}
|
|
||||||
>
|
|
||||||
<Text fontSize={16} fontWeight={500}>
|
|
||||||
{severity > 3 && !expertMode ? `Price Impact High` : `Send${severity > 2 ? ' Anyway' : ''}`}
|
|
||||||
</Text>
|
|
||||||
</ButtonError>
|
|
||||||
</RowBetween>
|
|
||||||
) : (
|
|
||||||
<ButtonError
|
|
||||||
onClick={() => {
|
|
||||||
expertMode ? (sendingWithSwap ? onSwap() : onSend()) : setShowConfirm(true)
|
|
||||||
}}
|
|
||||||
id="send-button"
|
|
||||||
disabled={
|
|
||||||
(sendingWithSwap && !isSwapValid) ||
|
|
||||||
(!sendingWithSwap && !isSendValid) ||
|
|
||||||
(severity > 3 && !expertMode && sendingWithSwap)
|
|
||||||
}
|
|
||||||
error={sendingWithSwap && isSwapValid && severity > 2}
|
|
||||||
>
|
|
||||||
<Text fontSize={20} fontWeight={500}>
|
|
||||||
{(sendingWithSwap ? swapError : null) ||
|
|
||||||
sendAmountError ||
|
|
||||||
recipientError ||
|
|
||||||
(severity > 3 && !expertMode && `Price Impact Too High`) ||
|
|
||||||
`Send${severity > 2 ? ' Anyway' : ''}`}
|
|
||||||
</Text>
|
|
||||||
</ButtonError>
|
|
||||||
)}
|
|
||||||
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
|
|
||||||
</BottomGrouping>
|
|
||||||
</Wrapper>
|
|
||||||
</AppBody>
|
|
||||||
|
|
||||||
<AdvancedSwapDetailsDropdown trade={bestTrade} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -4,12 +4,14 @@ import { ArrowDown } from 'react-feather'
|
|||||||
import ReactGA from 'react-ga'
|
import ReactGA from 'react-ga'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
import { ThemeContext } from 'styled-components'
|
import { ThemeContext } from 'styled-components'
|
||||||
|
import AddressInputPanel from '../../components/AddressInputPanel'
|
||||||
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
|
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
|
||||||
import Card, { GreyCard } from '../../components/Card'
|
import Card, { GreyCard } from '../../components/Card'
|
||||||
import { AutoColumn } from '../../components/Column'
|
import { AutoColumn } from '../../components/Column'
|
||||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||||
import { RowBetween } from '../../components/Row'
|
import { SwapPoolTabs } from '../../components/NavigationTabs'
|
||||||
|
import { AutoRow, RowBetween } from '../../components/Row'
|
||||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||||
import { ArrowWrapper, BottomGrouping, Dots, Wrapper } from '../../components/swap/styleds'
|
import { ArrowWrapper, BottomGrouping, Dots, Wrapper } from '../../components/swap/styleds'
|
||||||
@ -20,6 +22,7 @@ import BetterTradeLink from '../../components/swap/BetterTradeLink'
|
|||||||
import { TokenWarningCards } from '../../components/TokenWarningCard'
|
import { TokenWarningCards } from '../../components/TokenWarningCard'
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
|
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
|
||||||
|
import useENSAddress from '../../hooks/useENSAddress'
|
||||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||||
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
|
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||||
import { useExpertModeManager, useUserSlippageTolerance, useUserDeadline } from '../../state/user/hooks'
|
import { useExpertModeManager, useUserSlippageTolerance, useUserDeadline } from '../../state/user/hooks'
|
||||||
@ -34,7 +37,7 @@ import {
|
|||||||
useSwapActionHandlers,
|
useSwapActionHandlers,
|
||||||
useSwapState
|
useSwapState
|
||||||
} from '../../state/swap/hooks'
|
} from '../../state/swap/hooks'
|
||||||
import { CursorPointer, TYPE } from '../../theme'
|
import { CursorPointer, LinkStyledButton, TYPE } from '../../theme'
|
||||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||||
import AppBody from '../AppBody'
|
import AppBody from '../AppBody'
|
||||||
import { ClickableText } from '../Pool/styleds'
|
import { ClickableText } from '../Pool/styleds'
|
||||||
@ -57,10 +60,11 @@ export default function Swap() {
|
|||||||
const [allowedSlippage] = useUserSlippageTolerance()
|
const [allowedSlippage] = useUserSlippageTolerance()
|
||||||
|
|
||||||
// swap state
|
// swap state
|
||||||
const { independentField, typedValue } = useSwapState()
|
const { independentField, typedValue, recipient } = useSwapState()
|
||||||
const { bestTrade: bestTradeV2, tokenBalances, parsedAmount, tokens, error, v1Trade } = useDerivedSwapInfo()
|
const { bestTrade: bestTradeV2, tokenBalances, parsedAmount, tokens, error, v1Trade } = useDerivedSwapInfo()
|
||||||
|
const { address: recipientAddress } = useENSAddress(recipient)
|
||||||
const toggledVersion = useToggledVersion()
|
const toggledVersion = useToggledVersion()
|
||||||
const bestTrade = {
|
const trade = {
|
||||||
[Version.v1]: v1Trade,
|
[Version.v1]: v1Trade,
|
||||||
[Version.v2]: bestTradeV2
|
[Version.v2]: bestTradeV2
|
||||||
}[toggledVersion]
|
}[toggledVersion]
|
||||||
@ -73,11 +77,11 @@ export default function Swap() {
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const parsedAmounts = {
|
const parsedAmounts = {
|
||||||
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
|
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : trade?.inputAmount,
|
||||||
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
|
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : trade?.outputAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
|
const { onSwitchTokens, onTokenSelection, onUserInput, onChangeRecipient } = useSwapActionHandlers()
|
||||||
const isValid = !error
|
const isValid = !error
|
||||||
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||||
|
|
||||||
@ -91,7 +95,7 @@ export default function Swap() {
|
|||||||
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
|
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = bestTrade?.route
|
const route = trade?.route
|
||||||
const userHasSpecifiedInputOutput =
|
const userHasSpecifiedInputOutput =
|
||||||
!!tokens[Field.INPUT] &&
|
!!tokens[Field.INPUT] &&
|
||||||
!!tokens[Field.OUTPUT] &&
|
!!tokens[Field.OUTPUT] &&
|
||||||
@ -100,7 +104,7 @@ export default function Swap() {
|
|||||||
const noRoute = !route
|
const noRoute = !route
|
||||||
|
|
||||||
// check whether the user has approved the router on the input token
|
// check whether the user has approved the router on the input token
|
||||||
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
|
const [approval, approveCallback] = useApproveCallbackFromTrade(trade, allowedSlippage)
|
||||||
|
|
||||||
// check if user has gone through approval process, used to show two step buttons, reset on token change
|
// check if user has gone through approval process, used to show two step buttons, reset on token change
|
||||||
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
|
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
|
||||||
@ -126,12 +130,12 @@ export default function Swap() {
|
|||||||
const atMaxAmountInput: boolean =
|
const atMaxAmountInput: boolean =
|
||||||
maxAmountInput && parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
|
maxAmountInput && parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
|
||||||
|
|
||||||
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
|
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage)
|
||||||
|
|
||||||
// the callback to execute the swap
|
// the callback to execute the swap
|
||||||
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline)
|
const swapCallback = useSwapCallback(trade, allowedSlippage, deadline, recipient)
|
||||||
|
|
||||||
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(bestTrade)
|
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
|
||||||
|
|
||||||
function onSwap() {
|
function onSwap() {
|
||||||
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
|
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
|
||||||
@ -145,12 +149,13 @@ export default function Swap() {
|
|||||||
|
|
||||||
ReactGA.event({
|
ReactGA.event({
|
||||||
category: 'Swap',
|
category: 'Swap',
|
||||||
action: 'Swap w/o Send',
|
action:
|
||||||
label: [
|
recipient === null
|
||||||
bestTrade.inputAmount.token.symbol,
|
? 'Swap w/o Send'
|
||||||
bestTrade.outputAmount.token.symbol,
|
: (recipientAddress ?? recipient) === account
|
||||||
getTradeVersion(bestTrade)
|
? 'Swap w/o Send + recipient'
|
||||||
].join('/')
|
: 'Swap w/ Send',
|
||||||
|
label: [trade.inputAmount.token.symbol, trade.outputAmount.token.symbol, getTradeVersion(trade)].join('/')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@ -185,6 +190,7 @@ export default function Swap() {
|
|||||||
slippageAdjustedAmounts={slippageAdjustedAmounts}
|
slippageAdjustedAmounts={slippageAdjustedAmounts}
|
||||||
priceImpactSeverity={priceImpactSeverity}
|
priceImpactSeverity={priceImpactSeverity}
|
||||||
independentField={independentField}
|
independentField={independentField}
|
||||||
|
recipient={recipient}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -201,7 +207,7 @@ export default function Swap() {
|
|||||||
parsedAmounts={parsedAmounts}
|
parsedAmounts={parsedAmounts}
|
||||||
priceImpactWithoutFee={priceImpactWithoutFee}
|
priceImpactWithoutFee={priceImpactWithoutFee}
|
||||||
slippageAdjustedAmounts={slippageAdjustedAmounts}
|
slippageAdjustedAmounts={slippageAdjustedAmounts}
|
||||||
trade={bestTrade}
|
trade={trade}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -215,6 +221,7 @@ export default function Swap() {
|
|||||||
<>
|
<>
|
||||||
<TokenWarningCards tokens={tokens} />
|
<TokenWarningCards tokens={tokens} />
|
||||||
<AppBody>
|
<AppBody>
|
||||||
|
<SwapPoolTabs active={'swap'} />
|
||||||
<Wrapper id="swap-page">
|
<Wrapper id="swap-page">
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={showConfirm}
|
isOpen={showConfirm}
|
||||||
@ -235,7 +242,6 @@ export default function Swap() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<AutoColumn gap={'md'}>
|
<AutoColumn gap={'md'}>
|
||||||
<>
|
|
||||||
<CurrencyInputPanel
|
<CurrencyInputPanel
|
||||||
field={Field.INPUT}
|
field={Field.INPUT}
|
||||||
label={independentField === Field.OUTPUT ? 'From (estimated)' : 'From'}
|
label={independentField === Field.OUTPUT ? 'From (estimated)' : 'From'}
|
||||||
@ -255,8 +261,9 @@ export default function Swap() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<CursorPointer>
|
<CursorPointer>
|
||||||
<AutoColumn style={{ padding: '0 1rem' }}>
|
<AutoColumn justify="space-between">
|
||||||
<ArrowWrapper>
|
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
|
||||||
|
<ArrowWrapper clickable>
|
||||||
<ArrowDown
|
<ArrowDown
|
||||||
size="16"
|
size="16"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -266,6 +273,12 @@ export default function Swap() {
|
|||||||
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
|
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
|
||||||
/>
|
/>
|
||||||
</ArrowWrapper>
|
</ArrowWrapper>
|
||||||
|
{recipient === null ? (
|
||||||
|
<LinkStyledButton id="add-recipient-button" onClick={() => onChangeRecipient('')}>
|
||||||
|
+ add recipient
|
||||||
|
</LinkStyledButton>
|
||||||
|
) : null}
|
||||||
|
</AutoRow>
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
</CursorPointer>
|
</CursorPointer>
|
||||||
<CurrencyInputPanel
|
<CurrencyInputPanel
|
||||||
@ -279,7 +292,20 @@ export default function Swap() {
|
|||||||
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
|
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
|
||||||
id="swap-currency-output"
|
id="swap-currency-output"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{recipient !== null ? (
|
||||||
|
<>
|
||||||
|
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
|
||||||
|
<ArrowWrapper clickable={false}>
|
||||||
|
<ArrowDown size="16" color={theme.text2} />
|
||||||
|
</ArrowWrapper>
|
||||||
|
<LinkStyledButton id="remove-recipient-button" onClick={() => onChangeRecipient(null)}>
|
||||||
|
- remove recipient
|
||||||
|
</LinkStyledButton>
|
||||||
|
</AutoRow>
|
||||||
|
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
|
||||||
</>
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
|
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
|
||||||
<AutoColumn gap="4px">
|
<AutoColumn gap="4px">
|
||||||
@ -290,7 +316,7 @@ export default function Swap() {
|
|||||||
<TradePrice
|
<TradePrice
|
||||||
inputToken={tokens[Field.INPUT]}
|
inputToken={tokens[Field.INPUT]}
|
||||||
outputToken={tokens[Field.OUTPUT]}
|
outputToken={tokens[Field.OUTPUT]}
|
||||||
price={bestTrade?.executionPrice}
|
price={trade?.executionPrice}
|
||||||
showInverted={showInverted}
|
showInverted={showInverted}
|
||||||
setShowInverted={setShowInverted}
|
setShowInverted={setShowInverted}
|
||||||
/>
|
/>
|
||||||
@ -371,7 +397,7 @@ export default function Swap() {
|
|||||||
</Wrapper>
|
</Wrapper>
|
||||||
</AppBody>
|
</AppBody>
|
||||||
|
|
||||||
<AdvancedSwapDetailsDropdown trade={bestTrade} />
|
<AdvancedSwapDetailsDropdown trade={trade} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -13,4 +13,6 @@ export const replaceSwapState = createAction<{
|
|||||||
typedValue: string
|
typedValue: string
|
||||||
inputTokenAddress?: string
|
inputTokenAddress?: string
|
||||||
outputTokenAddress?: string
|
outputTokenAddress?: string
|
||||||
|
recipient: string | null
|
||||||
}>('replaceSwapState')
|
}>('replaceSwapState')
|
||||||
|
export const setRecipient = createAction<{ recipient: string | null }>('setRecipient')
|
||||||
|
@ -18,7 +18,8 @@ describe('hooks', () => {
|
|||||||
[Field.OUTPUT]: { address: '0x6B175474E89094C44Da98b954EedeAC495271d0F' },
|
[Field.OUTPUT]: { address: '0x6B175474E89094C44Da98b954EedeAC495271d0F' },
|
||||||
[Field.INPUT]: { address: WETH[ChainId.MAINNET].address },
|
[Field.INPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||||
typedValue: '20.5',
|
typedValue: '20.5',
|
||||||
independentField: Field.OUTPUT
|
independentField: Field.OUTPUT,
|
||||||
|
recipient: null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -32,7 +33,8 @@ describe('hooks', () => {
|
|||||||
[Field.INPUT]: { address: '' },
|
[Field.INPUT]: { address: '' },
|
||||||
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||||
typedValue: '',
|
typedValue: '',
|
||||||
independentField: Field.INPUT
|
independentField: Field.INPUT,
|
||||||
|
recipient: null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -46,7 +48,58 @@ describe('hooks', () => {
|
|||||||
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||||
[Field.INPUT]: { address: '' },
|
[Field.INPUT]: { address: '' },
|
||||||
typedValue: '20.5',
|
typedValue: '20.5',
|
||||||
independentField: Field.INPUT
|
independentField: Field.INPUT,
|
||||||
|
recipient: null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('invalid recipient', () => {
|
||||||
|
expect(
|
||||||
|
queryParametersToSwapState(
|
||||||
|
parse('?outputCurrency=eth&exactAmount=20.5&recipient=abc', { parseArrays: false, ignoreQueryPrefix: true }),
|
||||||
|
ChainId.MAINNET
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||||
|
[Field.INPUT]: { address: '' },
|
||||||
|
typedValue: '20.5',
|
||||||
|
independentField: Field.INPUT,
|
||||||
|
recipient: null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('valid recipient', () => {
|
||||||
|
expect(
|
||||||
|
queryParametersToSwapState(
|
||||||
|
parse('?outputCurrency=eth&exactAmount=20.5&recipient=0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5', {
|
||||||
|
parseArrays: false,
|
||||||
|
ignoreQueryPrefix: true
|
||||||
|
}),
|
||||||
|
ChainId.MAINNET
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||||
|
[Field.INPUT]: { address: '' },
|
||||||
|
typedValue: '20.5',
|
||||||
|
independentField: Field.INPUT,
|
||||||
|
recipient: '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test('accepts any recipient', () => {
|
||||||
|
expect(
|
||||||
|
queryParametersToSwapState(
|
||||||
|
parse('?outputCurrency=eth&exactAmount=20.5&recipient=bob.argent.xyz', {
|
||||||
|
parseArrays: false,
|
||||||
|
ignoreQueryPrefix: true
|
||||||
|
}),
|
||||||
|
ChainId.MAINNET
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||||
|
[Field.INPUT]: { address: '' },
|
||||||
|
typedValue: '20.5',
|
||||||
|
independentField: Field.INPUT,
|
||||||
|
recipient: 'bob.argent.xyz'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Version } from './../../hooks/useToggledVersion'
|
import useENS from '../../hooks/useENS'
|
||||||
|
import { Version } from '../../hooks/useToggledVersion'
|
||||||
import { parseUnits } from '@ethersproject/units'
|
import { parseUnits } from '@ethersproject/units'
|
||||||
import { ChainId, JSBI, Token, TokenAmount, Trade, WETH } from '@uniswap/sdk'
|
import { ChainId, JSBI, Token, TokenAmount, Trade, WETH } from '@uniswap/sdk'
|
||||||
import { ParsedQs } from 'qs'
|
import { ParsedQs } from 'qs'
|
||||||
@ -12,7 +13,7 @@ import useParsedQueryString from '../../hooks/useParsedQueryString'
|
|||||||
import { isAddress } from '../../utils'
|
import { isAddress } from '../../utils'
|
||||||
import { AppDispatch, AppState } from '../index'
|
import { AppDispatch, AppState } from '../index'
|
||||||
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
|
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
|
||||||
import { Field, replaceSwapState, selectToken, switchTokens, typeInput } from './actions'
|
import { Field, replaceSwapState, selectToken, setRecipient, switchTokens, typeInput } from './actions'
|
||||||
import { SwapState } from './reducer'
|
import { SwapState } from './reducer'
|
||||||
import useToggledVersion from '../../hooks/useToggledVersion'
|
import useToggledVersion from '../../hooks/useToggledVersion'
|
||||||
import { useUserSlippageTolerance } from '../user/hooks'
|
import { useUserSlippageTolerance } from '../user/hooks'
|
||||||
@ -26,6 +27,7 @@ export function useSwapActionHandlers(): {
|
|||||||
onTokenSelection: (field: Field, address: string) => void
|
onTokenSelection: (field: Field, address: string) => void
|
||||||
onSwitchTokens: () => void
|
onSwitchTokens: () => void
|
||||||
onUserInput: (field: Field, typedValue: string) => void
|
onUserInput: (field: Field, typedValue: string) => void
|
||||||
|
onChangeRecipient: (recipient: string | null) => void
|
||||||
} {
|
} {
|
||||||
const dispatch = useDispatch<AppDispatch>()
|
const dispatch = useDispatch<AppDispatch>()
|
||||||
const onTokenSelection = useCallback(
|
const onTokenSelection = useCallback(
|
||||||
@ -51,10 +53,18 @@ export function useSwapActionHandlers(): {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onChangeRecipient = useCallback(
|
||||||
|
(recipient: string | null) => {
|
||||||
|
dispatch(setRecipient({ recipient }))
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onSwitchTokens,
|
onSwitchTokens,
|
||||||
onTokenSelection,
|
onTokenSelection,
|
||||||
onUserInput
|
onUserInput,
|
||||||
|
onChangeRecipient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,11 +103,14 @@ export function useDerivedSwapInfo(): {
|
|||||||
independentField,
|
independentField,
|
||||||
typedValue,
|
typedValue,
|
||||||
[Field.INPUT]: { address: tokenInAddress },
|
[Field.INPUT]: { address: tokenInAddress },
|
||||||
[Field.OUTPUT]: { address: tokenOutAddress }
|
[Field.OUTPUT]: { address: tokenOutAddress },
|
||||||
|
recipient
|
||||||
} = useSwapState()
|
} = useSwapState()
|
||||||
|
|
||||||
const tokenIn = useToken(tokenInAddress)
|
const tokenIn = useToken(tokenInAddress)
|
||||||
const tokenOut = useToken(tokenOutAddress)
|
const tokenOut = useToken(tokenOutAddress)
|
||||||
|
const recipientLookup = useENS(recipient ?? undefined)
|
||||||
|
const to: string | null = (recipient === null ? account : recipientLookup.address) ?? null
|
||||||
|
|
||||||
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [
|
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [
|
||||||
tokenIn ?? undefined,
|
tokenIn ?? undefined,
|
||||||
@ -138,6 +151,10 @@ export function useDerivedSwapInfo(): {
|
|||||||
error = error ?? 'Select a token'
|
error = error ?? 'Select a token'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!to) {
|
||||||
|
error = error ?? 'Enter a recipient'
|
||||||
|
}
|
||||||
|
|
||||||
const [allowedSlippage] = useUserSlippageTolerance()
|
const [allowedSlippage] = useUserSlippageTolerance()
|
||||||
|
|
||||||
const slippageAdjustedAmounts =
|
const slippageAdjustedAmounts =
|
||||||
@ -172,14 +189,14 @@ export function useDerivedSwapInfo(): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCurrencyFromURLParameter(urlParam: any, chainId: number, overrideWETH: boolean): string {
|
function parseCurrencyFromURLParameter(urlParam: any, chainId: number): string {
|
||||||
if (typeof urlParam === 'string') {
|
if (typeof urlParam === 'string') {
|
||||||
const valid = isAddress(urlParam)
|
const valid = isAddress(urlParam)
|
||||||
if (valid) return valid
|
if (valid) return valid
|
||||||
if (urlParam.toLowerCase() === 'eth') return WETH[chainId as ChainId]?.address ?? ''
|
if (urlParam.toLowerCase() === 'eth') return WETH[chainId as ChainId]?.address ?? ''
|
||||||
if (valid === false) return WETH[chainId as ChainId]?.address ?? ''
|
if (valid === false) return WETH[chainId as ChainId]?.address ?? ''
|
||||||
}
|
}
|
||||||
return overrideWETH ? '' : WETH[chainId as ChainId]?.address ?? ''
|
return WETH[chainId as ChainId]?.address ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTokenAmountURLParameter(urlParam: any): string {
|
function parseTokenAmountURLParameter(urlParam: any): string {
|
||||||
@ -190,9 +207,20 @@ function parseIndependentFieldURLParameter(urlParam: any): Field {
|
|||||||
return typeof urlParam === 'string' && urlParam.toLowerCase() === 'output' ? Field.OUTPUT : Field.INPUT
|
return typeof urlParam === 'string' && urlParam.toLowerCase() === 'output' ? Field.OUTPUT : Field.INPUT
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queryParametersToSwapState(parsedQs: ParsedQs, chainId: ChainId, overrideETH: boolean): SwapState {
|
const ENS_NAME_REGEX = /^[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)?$/
|
||||||
let inputCurrency = parseCurrencyFromURLParameter(parsedQs.inputCurrency, chainId, overrideETH)
|
const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/
|
||||||
let outputCurrency = parseCurrencyFromURLParameter(parsedQs.outputCurrency, chainId, overrideETH)
|
function validatedRecipient(recipient: any): string | null {
|
||||||
|
if (typeof recipient !== 'string') return null
|
||||||
|
const address = isAddress(recipient)
|
||||||
|
if (address) return address
|
||||||
|
if (ENS_NAME_REGEX.test(recipient)) return recipient
|
||||||
|
if (ADDRESS_REGEX.test(recipient)) return recipient
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryParametersToSwapState(parsedQs: ParsedQs, chainId: ChainId): SwapState {
|
||||||
|
let inputCurrency = parseCurrencyFromURLParameter(parsedQs.inputCurrency, chainId)
|
||||||
|
let outputCurrency = parseCurrencyFromURLParameter(parsedQs.outputCurrency, chainId)
|
||||||
if (inputCurrency === outputCurrency) {
|
if (inputCurrency === outputCurrency) {
|
||||||
if (typeof parsedQs.outputCurrency === 'string') {
|
if (typeof parsedQs.outputCurrency === 'string') {
|
||||||
inputCurrency = ''
|
inputCurrency = ''
|
||||||
@ -201,6 +229,8 @@ export function queryParametersToSwapState(parsedQs: ParsedQs, chainId: ChainId,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipient = validatedRecipient(parsedQs.recipient)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[Field.INPUT]: {
|
[Field.INPUT]: {
|
||||||
address: inputCurrency
|
address: inputCurrency
|
||||||
@ -209,29 +239,29 @@ export function queryParametersToSwapState(parsedQs: ParsedQs, chainId: ChainId,
|
|||||||
address: outputCurrency
|
address: outputCurrency
|
||||||
},
|
},
|
||||||
typedValue: parseTokenAmountURLParameter(parsedQs.exactAmount),
|
typedValue: parseTokenAmountURLParameter(parsedQs.exactAmount),
|
||||||
independentField: parseIndependentFieldURLParameter(parsedQs.exactField)
|
independentField: parseIndependentFieldURLParameter(parsedQs.exactField),
|
||||||
|
recipient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// updates the swap state to use the defaults for a given network
|
// updates the swap state to use the defaults for a given network
|
||||||
// set overrideETH to true if dont want to autopopulate ETH
|
export function useDefaultsFromURLSearch() {
|
||||||
export function useDefaultsFromURLSearch(overrideWETH = false) {
|
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
const dispatch = useDispatch<AppDispatch>()
|
const dispatch = useDispatch<AppDispatch>()
|
||||||
const parsedQs = useParsedQueryString()
|
const parsedQs = useParsedQueryString()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chainId) return
|
if (!chainId) return
|
||||||
const parsed = queryParametersToSwapState(parsedQs, chainId, overrideWETH)
|
const parsed = queryParametersToSwapState(parsedQs, chainId)
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
replaceSwapState({
|
replaceSwapState({
|
||||||
typedValue: parsed.typedValue,
|
typedValue: parsed.typedValue,
|
||||||
field: parsed.independentField,
|
field: parsed.independentField,
|
||||||
inputTokenAddress: parsed[Field.INPUT].address,
|
inputTokenAddress: parsed[Field.INPUT].address,
|
||||||
outputTokenAddress: parsed[Field.OUTPUT].address
|
outputTokenAddress: parsed[Field.OUTPUT].address,
|
||||||
|
recipient: parsed.recipient
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
// eslint-disable-next-line
|
}, [dispatch, chainId, parsedQs])
|
||||||
}, [dispatch, chainId])
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createReducer } from '@reduxjs/toolkit'
|
import { createReducer } from '@reduxjs/toolkit'
|
||||||
import { Field, replaceSwapState, selectToken, switchTokens, typeInput } from './actions'
|
import { Field, replaceSwapState, selectToken, setRecipient, switchTokens, typeInput } from './actions'
|
||||||
|
|
||||||
export interface SwapState {
|
export interface SwapState {
|
||||||
readonly independentField: Field
|
readonly independentField: Field
|
||||||
@ -10,6 +10,8 @@ export interface SwapState {
|
|||||||
readonly [Field.OUTPUT]: {
|
readonly [Field.OUTPUT]: {
|
||||||
readonly address: string | undefined
|
readonly address: string | undefined
|
||||||
}
|
}
|
||||||
|
// the typed recipient address, or null if swap should go to sender
|
||||||
|
readonly recipient: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SwapState = {
|
const initialState: SwapState = {
|
||||||
@ -20,12 +22,15 @@ const initialState: SwapState = {
|
|||||||
},
|
},
|
||||||
[Field.OUTPUT]: {
|
[Field.OUTPUT]: {
|
||||||
address: ''
|
address: ''
|
||||||
}
|
},
|
||||||
|
recipient: null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createReducer<SwapState>(initialState, builder =>
|
export default createReducer<SwapState>(initialState, builder =>
|
||||||
builder
|
builder
|
||||||
.addCase(replaceSwapState, (state, { payload: { typedValue, field, inputTokenAddress, outputTokenAddress } }) => {
|
.addCase(
|
||||||
|
replaceSwapState,
|
||||||
|
(state, { payload: { typedValue, recipient, field, inputTokenAddress, outputTokenAddress } }) => {
|
||||||
return {
|
return {
|
||||||
[Field.INPUT]: {
|
[Field.INPUT]: {
|
||||||
address: inputTokenAddress
|
address: inputTokenAddress
|
||||||
@ -34,9 +39,11 @@ export default createReducer<SwapState>(initialState, builder =>
|
|||||||
address: outputTokenAddress
|
address: outputTokenAddress
|
||||||
},
|
},
|
||||||
independentField: field,
|
independentField: field,
|
||||||
typedValue: typedValue
|
typedValue: typedValue,
|
||||||
|
recipient
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.addCase(selectToken, (state, { payload: { address, field } }) => {
|
.addCase(selectToken, (state, { payload: { address, field } }) => {
|
||||||
const otherField = field === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
const otherField = field === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||||
if (address === state[otherField].address) {
|
if (address === state[otherField].address) {
|
||||||
@ -70,4 +77,7 @@ export default createReducer<SwapState>(initialState, builder =>
|
|||||||
typedValue
|
typedValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.addCase(setRecipient, (state, { payload: { recipient } }) => {
|
||||||
|
state.recipient = recipient
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
@ -39,7 +39,7 @@ export function useAllTransactions(): { [txHash: string]: TransactionDetails } {
|
|||||||
|
|
||||||
const state = useSelector<AppState, TransactionState>(state => state.transactions)
|
const state = useSelector<AppState, TransactionState>(state => state.transactions)
|
||||||
|
|
||||||
return state[chainId ?? -1] ?? {}
|
return chainId ? state[chainId] ?? {} : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useIsTransactionPending(transactionHash?: string): boolean {
|
export function useIsTransactionPending(transactionHash?: string): boolean {
|
||||||
|
@ -188,6 +188,10 @@ body {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-variant: none;
|
font-variant: none;
|
||||||
|
Loading…
Reference in New Issue
Block a user