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', () => {
|
||||
beforeEach(() => cy.visit('/send'))
|
||||
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#sending-no-swap-input')
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
it('should redirect', () => {
|
||||
cy.url().should('include', '/swap')
|
||||
})
|
||||
})
|
||||
|
@ -40,4 +40,15 @@ describe('Swap', () => {
|
||||
cy.get('#swap-button').click()
|
||||
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 styled from 'styled-components'
|
||||
import { CheckCircle, Triangle, ExternalLink as LinkIcon } from 'react-feather'
|
||||
import { CheckCircle, Triangle } from 'react-feather'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
@ -50,8 +50,7 @@ export default function Transaction({ hash }: { hash: string }) {
|
||||
<TransactionWrapper>
|
||||
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
|
||||
<RowFixed>
|
||||
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText>
|
||||
<LinkIcon size={16} />
|
||||
<TransactionStatusText>{summary ?? hash} ↗</TransactionStatusText>
|
||||
</RowFixed>
|
||||
<IconWrapper pending={pending} success={success}>
|
||||
{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 useDebounce from '../../hooks/useDebounce'
|
||||
|
||||
import { isAddress } from '../../utils'
|
||||
import useENS from '../../hooks/useENS'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
@ -24,6 +22,8 @@ const ContainerRow = styled.div<{ error: boolean }>`
|
||||
align-items: center;
|
||||
border-radius: 1.25rem;
|
||||
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};
|
||||
`
|
||||
|
||||
@ -39,6 +39,7 @@ const Input = styled.input<{ error?: boolean }>`
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||
color: ${({ error, theme }) => (error ? theme.red1 : theme.primary1)};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -64,120 +65,65 @@ const Input = styled.input<{ error?: boolean }>`
|
||||
}
|
||||
`
|
||||
|
||||
interface Value {
|
||||
address: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export default function AddressInputPanel({
|
||||
initialInput = '',
|
||||
onChange,
|
||||
onError
|
||||
id,
|
||||
value,
|
||||
onChange
|
||||
}: {
|
||||
initialInput?: string
|
||||
onChange: (val: { address: string; name?: string }) => void
|
||||
onError: (error: boolean, input: string) => void
|
||||
id?: string
|
||||
// the typed string value
|
||||
value: string
|
||||
// triggers whenever the typed value changes
|
||||
onChange: (value: string) => void
|
||||
}) {
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [input, setInput] = useState(initialInput ? initialInput : '')
|
||||
const debouncedInput = useDebounce(input, 200)
|
||||
const { address, loading, name } = useENS(value)
|
||||
|
||||
const [data, setData] = useState<{ address: string; name: string }>({ address: undefined, name: undefined })
|
||||
const [error, setError] = useState<boolean>(false)
|
||||
const handleInput = useCallback(
|
||||
event => {
|
||||
const input = event.target.value
|
||||
const withoutSpaces = input.replace(/\s+/g, '')
|
||||
onChange(withoutSpaces)
|
||||
},
|
||||
[onChange]
|
||||
)
|
||||
|
||||
// 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 checksummedInput = isAddress(input.replace(/\s/g, '')) // delete whitespace
|
||||
setInput(checksummedInput || input)
|
||||
}
|
||||
const error = Boolean(value.length > 0 && !loading && !address)
|
||||
|
||||
return (
|
||||
<InputPanel>
|
||||
<ContainerRow error={input !== '' && error}>
|
||||
<InputPanel id={id}>
|
||||
<ContainerRow error={error}>
|
||||
<InputContainer>
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
|
||||
Recipient
|
||||
</TYPE.black>
|
||||
{data.address && (
|
||||
<ExternalLink
|
||||
href={getEtherscanLink(chainId, data.name || data.address, 'address')}
|
||||
style={{ fontSize: '14px' }}
|
||||
>
|
||||
{address && (
|
||||
<ExternalLink href={getEtherscanLink(chainId, name ?? address, 'address')} style={{ fontSize: '14px' }}>
|
||||
(View on Etherscan)
|
||||
</ExternalLink>
|
||||
)}
|
||||
</RowBetween>
|
||||
<Input
|
||||
className="recipient-address-input"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
placeholder="Wallet Address or ENS name"
|
||||
error={input !== '' && error}
|
||||
onChange={onInput}
|
||||
value={input}
|
||||
error={error}
|
||||
pattern="^(0x[a-fA-F0-9]{40})$"
|
||||
onChange={handleInput}
|
||||
value={value}
|
||||
/>
|
||||
</AutoColumn>
|
||||
</InputContainer>
|
||||
|
@ -1,37 +1,18 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { withRouter, NavLink, Link as HistoryLink, RouteComponentProps } from 'react-router-dom'
|
||||
import useBodyKeyDown from '../../hooks/useBodyKeyDown'
|
||||
import { NavLink, Link as HistoryLink } from 'react-router-dom'
|
||||
|
||||
import { CursorPointer } from '../../theme'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { RowBetween } from '../Row'
|
||||
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`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
border-radius: 3rem;
|
||||
justify-content: space-evenly;
|
||||
`
|
||||
|
||||
const activeClassName = 'ACTIVE'
|
||||
@ -43,7 +24,6 @@ const StyledNavLink = styled(NavLink).attrs({
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 3rem;
|
||||
flex: 1 0 auto;
|
||||
border-radius: 3rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
@ -68,89 +48,68 @@ const ActiveText = styled.div`
|
||||
font-size: 20px;
|
||||
`
|
||||
|
||||
const ArrowLink = styled(ArrowLeft)`
|
||||
const StyledArrowLeft = styled(ArrowLeft)`
|
||||
color: ${({ theme }) => theme.text1};
|
||||
`
|
||||
|
||||
function NavigationTabs({ location: { pathname }, history }: RouteComponentProps<{}>) {
|
||||
export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) {
|
||||
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 (
|
||||
<>
|
||||
{adding || removing ? (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<CursorPointer onClick={() => history.push('/pool')}>
|
||||
<ArrowLink />
|
||||
</CursorPointer>
|
||||
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
|
||||
<QuestionHelper
|
||||
text={
|
||||
adding
|
||||
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
|
||||
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
|
||||
}
|
||||
/>
|
||||
</RowBetween>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
<Tabs style={{ marginBottom: '20px' }}>
|
||||
<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 default withRouter(NavigationTabs)
|
||||
export function CreatePoolTabs() {
|
||||
return (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<StyledArrowLeft />
|
||||
</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>
|
||||
<QuestionHelper
|
||||
text={
|
||||
adding
|
||||
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
|
||||
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
|
||||
}
|
||||
/>
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
@ -62,9 +62,7 @@ export default function TxnPopup({
|
||||
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
|
||||
</div>
|
||||
<AutoColumn gap="8px">
|
||||
<TYPE.body fontWeight={500}>
|
||||
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
|
||||
</TYPE.body>
|
||||
<TYPE.body fontWeight={500}>{summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
||||
</AutoColumn>
|
||||
<Fader count={count} />
|
||||
|
@ -130,7 +130,7 @@ export default function Web3Status() {
|
||||
const { active, account, connector, error } = useWeb3React()
|
||||
const contextNetwork = useWeb3React(NetworkContextName)
|
||||
|
||||
const ENSName = useENSName(account)
|
||||
const { ENSName } = useENSName(account)
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { TYPE } from '../../theme'
|
||||
import { isAddress, shortenAddress } from '../../utils'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
@ -15,13 +16,15 @@ export default function SwapModalHeader({
|
||||
formattedAmounts,
|
||||
slippageAdjustedAmounts,
|
||||
priceImpactSeverity,
|
||||
independentField
|
||||
independentField,
|
||||
recipient
|
||||
}: {
|
||||
tokens: { [field in Field]?: Token }
|
||||
formattedAmounts: { [field in Field]?: string }
|
||||
slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
|
||||
priceImpactSeverity: number
|
||||
independentField: Field
|
||||
recipient: string | null
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
@ -71,6 +74,14 @@ export default function SwapModalHeader({
|
||||
</TYPE.italic>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
@ -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 { Text } from 'rebass'
|
||||
|
||||
@ -8,17 +8,18 @@ export const Wrapper = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const ArrowWrapper = styled.div`
|
||||
export const ArrowWrapper = styled.div<{ clickable: boolean }>`
|
||||
padding: 2px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
${({ clickable }) =>
|
||||
clickable
|
||||
? css`
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
`
|
||||
: null}
|
||||
`
|
||||
|
||||
export const SectionBreak = styled.div`
|
||||
|
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.
|
||||
* 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 [ENSName, setENSName] = useState<string | null>(null)
|
||||
const [ENSName, setENSName] = useState<{ ENSName: string | null; loading: boolean }>({
|
||||
loading: false,
|
||||
ENSName: null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!library || !address) return
|
||||
const validated = isAddress(address)
|
||||
if (validated) {
|
||||
if (!library || !validated) {
|
||||
setENSName({ loading: false, ENSName: null })
|
||||
return
|
||||
} else {
|
||||
let stale = false
|
||||
setENSName({ loading: true, ENSName: null })
|
||||
library
|
||||
.lookupAddress(validated)
|
||||
.then(name => {
|
||||
if (!stale) {
|
||||
if (name) {
|
||||
setENSName(name)
|
||||
setENSName({ loading: false, ENSName: name })
|
||||
} else {
|
||||
setENSName(null)
|
||||
setENSName({ loading: false, ENSName: null })
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setENSName(null)
|
||||
setENSName({ loading: false, ENSName: null })
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
setENSName(null)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [library, address])
|
||||
|
||||
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 { MaxUint256 } from '@ethersproject/constants'
|
||||
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 { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
|
||||
import { useTokenAllowance } from '../data/Allowances'
|
||||
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||
import { Field } from '../state/swap/actions'
|
||||
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 { useActiveWeb3React } from './index'
|
||||
import { useV1ExchangeContract } from './useContract'
|
||||
import useENSName from './useENSName'
|
||||
import useENS from './useENS'
|
||||
import { Version } from './useToggledVersion'
|
||||
|
||||
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
|
||||
// and the user has approved the slippage adjusted input amount for the trade
|
||||
export function useSwapCallback(
|
||||
trade?: Trade, // trade to execute, required
|
||||
trade: Trade | undefined, // trade to execute, required
|
||||
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
|
||||
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>) {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
const addTransaction = useTransactionAdder()
|
||||
const recipient = to ? isAddress(to) : account
|
||||
const ensName = useENSName(to)
|
||||
|
||||
const { address: recipient } = useENS(recipientAddressOrName)
|
||||
|
||||
const tradeVersion = getTradeVersion(trade)
|
||||
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
|
||||
const inputAllowance = useTokenAllowance(
|
||||
@ -77,7 +78,7 @@ export function useSwapCallback(
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trade || !recipient || !tradeVersion) return null
|
||||
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null
|
||||
|
||||
// will always be defined
|
||||
const {
|
||||
@ -89,17 +90,13 @@ export function useSwapCallback(
|
||||
|
||||
// no allowance
|
||||
if (
|
||||
!trade.inputAmount.token.equals(WETH[chainId as ChainId]) &&
|
||||
!trade.inputAmount.token.equals(WETH[chainId]) &&
|
||||
(!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return async function onSwap() {
|
||||
if (!chainId || !library || !account) {
|
||||
throw new Error('missing dependencies in onSwap callback')
|
||||
}
|
||||
|
||||
const contract: Contract | null =
|
||||
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
|
||||
if (!contract) {
|
||||
@ -283,7 +280,12 @@ export function useSwapCallback(
|
||||
const outputAmount = slippageAdjustedOutput.toSignificant(3)
|
||||
|
||||
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 =
|
||||
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
|
||||
@ -310,15 +312,15 @@ export function useSwapCallback(
|
||||
}, [
|
||||
trade,
|
||||
recipient,
|
||||
tradeVersion,
|
||||
allowedSlippage,
|
||||
chainId,
|
||||
inputAllowance,
|
||||
library,
|
||||
account,
|
||||
tradeVersion,
|
||||
chainId,
|
||||
allowedSlippage,
|
||||
inputAllowance,
|
||||
v1Exchange,
|
||||
deadline,
|
||||
addTransaction,
|
||||
ensName
|
||||
recipientAddressOrName,
|
||||
addTransaction
|
||||
])
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import { Field } from '../../state/mint/actions'
|
||||
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { useUserSlippageTolerance, useUserDeadline, useIsExpertMode } from '../../state/user/hooks'
|
||||
import { AddRemoveTabs } from '../../components/NavigationTabs'
|
||||
|
||||
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
|
||||
useDefaultsFromURLMatchParams(params)
|
||||
@ -307,6 +308,7 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
return (
|
||||
<>
|
||||
<AppBody>
|
||||
<AddRemoveTabs adding={true} />
|
||||
<Wrapper>
|
||||
<ConfirmationModal
|
||||
isOpen={showConfirm}
|
||||
|
@ -14,7 +14,6 @@ import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange'
|
||||
import Pool from './Pool'
|
||||
import PoolFinder from './PoolFinder'
|
||||
import RemoveLiquidity from './RemoveLiquidity'
|
||||
import Send from './Send'
|
||||
import Swap from './Swap'
|
||||
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
|
||||
|
||||
@ -69,7 +68,7 @@ export default function App() {
|
||||
<Switch>
|
||||
<Route exact strict path="/swap" component={Swap} />
|
||||
<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="/pool" component={Pool} />
|
||||
<Route exact strict path="/create" component={CreatePool} />
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import NavigationTabs from '../components/NavigationTabs'
|
||||
|
||||
export const BodyWrapper = styled.div`
|
||||
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.
|
||||
*/
|
||||
export default function AppBody({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<BodyWrapper>
|
||||
<NavigationTabs />
|
||||
<>{children}</>
|
||||
</BodyWrapper>
|
||||
)
|
||||
return <BodyWrapper>{children}</BodyWrapper>
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { RouteComponentProps, Redirect } from 'react-router-dom'
|
||||
import { Token, WETH } from '@uniswap/sdk'
|
||||
import { CreatePoolTabs } from '../../components/NavigationTabs'
|
||||
import AppBody from '../AppBody'
|
||||
|
||||
import Row, { AutoRow } from '../../components/Row'
|
||||
@ -56,6 +57,7 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
<CreatePoolTabs />
|
||||
<AutoColumn gap="20px">
|
||||
<AutoColumn gap="24px">
|
||||
{!token0Address ? (
|
||||
|
@ -2,6 +2,7 @@ import React, { useState, useContext, useCallback } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { JSBI } from '@uniswap/sdk'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { SwapPoolTabs } from '../../components/NavigationTabs'
|
||||
|
||||
import Question from '../../components/QuestionHelper'
|
||||
import PairSearchModal from '../../components/SearchModal/PairSearchModal'
|
||||
@ -70,6 +71,7 @@ export default function Pool({ history }: RouteComponentProps) {
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
<SwapPoolTabs active={'pool'} />
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
<ButtonPrimary
|
||||
id="join-pool-button"
|
||||
|
@ -5,6 +5,7 @@ import { Text } from 'rebass'
|
||||
import { ButtonDropdownLight } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import { FindPoolTabs } from '../../components/NavigationTabs'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
import Row from '../../components/Row'
|
||||
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
|
||||
@ -61,6 +62,7 @@ export default function PoolFinder() {
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
<FindPoolTabs />
|
||||
<AutoColumn gap="md">
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
|
@ -13,6 +13,7 @@ import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import DoubleLogo from '../../components/DoubleLogo'
|
||||
import { AddRemoveTabs } from '../../components/NavigationTabs'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
import Row, { RowBetween, RowFixed } from '../../components/Row'
|
||||
|
||||
@ -390,6 +391,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
|
||||
return (
|
||||
<>
|
||||
<AppBody>
|
||||
<AddRemoveTabs adding={false} />
|
||||
<Wrapper>
|
||||
<ConfirmationModal
|
||||
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 { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import AddressInputPanel from '../../components/AddressInputPanel'
|
||||
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
|
||||
import Card, { GreyCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import { RowBetween } from '../../components/Row'
|
||||
import { SwapPoolTabs } from '../../components/NavigationTabs'
|
||||
import { AutoRow, RowBetween } from '../../components/Row'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||
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 { useActiveWeb3React } from '../../hooks'
|
||||
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
|
||||
import useENSAddress from '../../hooks/useENSAddress'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import { useExpertModeManager, useUserSlippageTolerance, useUserDeadline } from '../../state/user/hooks'
|
||||
@ -34,7 +37,7 @@ import {
|
||||
useSwapActionHandlers,
|
||||
useSwapState
|
||||
} from '../../state/swap/hooks'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { CursorPointer, LinkStyledButton, TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { ClickableText } from '../Pool/styleds'
|
||||
@ -57,10 +60,11 @@ export default function Swap() {
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
// swap state
|
||||
const { independentField, typedValue } = useSwapState()
|
||||
const { independentField, typedValue, recipient } = useSwapState()
|
||||
const { bestTrade: bestTradeV2, tokenBalances, parsedAmount, tokens, error, v1Trade } = useDerivedSwapInfo()
|
||||
const { address: recipientAddress } = useENSAddress(recipient)
|
||||
const toggledVersion = useToggledVersion()
|
||||
const bestTrade = {
|
||||
const trade = {
|
||||
[Version.v1]: v1Trade,
|
||||
[Version.v2]: bestTradeV2
|
||||
}[toggledVersion]
|
||||
@ -73,11 +77,11 @@ export default function Swap() {
|
||||
: undefined
|
||||
|
||||
const parsedAmounts = {
|
||||
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
|
||||
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
|
||||
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : trade?.inputAmount,
|
||||
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : trade?.outputAmount
|
||||
}
|
||||
|
||||
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
|
||||
const { onSwitchTokens, onTokenSelection, onUserInput, onChangeRecipient } = useSwapActionHandlers()
|
||||
const isValid = !error
|
||||
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) : ''
|
||||
}
|
||||
|
||||
const route = bestTrade?.route
|
||||
const route = trade?.route
|
||||
const userHasSpecifiedInputOutput =
|
||||
!!tokens[Field.INPUT] &&
|
||||
!!tokens[Field.OUTPUT] &&
|
||||
@ -100,7 +104,7 @@ export default function Swap() {
|
||||
const noRoute = !route
|
||||
|
||||
// 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
|
||||
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
|
||||
@ -126,12 +130,12 @@ export default function Swap() {
|
||||
const atMaxAmountInput: boolean =
|
||||
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
|
||||
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() {
|
||||
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
|
||||
@ -145,12 +149,13 @@ export default function Swap() {
|
||||
|
||||
ReactGA.event({
|
||||
category: 'Swap',
|
||||
action: 'Swap w/o Send',
|
||||
label: [
|
||||
bestTrade.inputAmount.token.symbol,
|
||||
bestTrade.outputAmount.token.symbol,
|
||||
getTradeVersion(bestTrade)
|
||||
].join('/')
|
||||
action:
|
||||
recipient === null
|
||||
? 'Swap w/o Send'
|
||||
: (recipientAddress ?? recipient) === account
|
||||
? 'Swap w/o Send + recipient'
|
||||
: 'Swap w/ Send',
|
||||
label: [trade.inputAmount.token.symbol, trade.outputAmount.token.symbol, getTradeVersion(trade)].join('/')
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
@ -185,6 +190,7 @@ export default function Swap() {
|
||||
slippageAdjustedAmounts={slippageAdjustedAmounts}
|
||||
priceImpactSeverity={priceImpactSeverity}
|
||||
independentField={independentField}
|
||||
recipient={recipient}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -201,7 +207,7 @@ export default function Swap() {
|
||||
parsedAmounts={parsedAmounts}
|
||||
priceImpactWithoutFee={priceImpactWithoutFee}
|
||||
slippageAdjustedAmounts={slippageAdjustedAmounts}
|
||||
trade={bestTrade}
|
||||
trade={trade}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -215,6 +221,7 @@ export default function Swap() {
|
||||
<>
|
||||
<TokenWarningCards tokens={tokens} />
|
||||
<AppBody>
|
||||
<SwapPoolTabs active={'swap'} />
|
||||
<Wrapper id="swap-page">
|
||||
<ConfirmationModal
|
||||
isOpen={showConfirm}
|
||||
@ -235,28 +242,28 @@ export default function Swap() {
|
||||
/>
|
||||
|
||||
<AutoColumn gap={'md'}>
|
||||
<>
|
||||
<CurrencyInputPanel
|
||||
field={Field.INPUT}
|
||||
label={independentField === Field.OUTPUT ? 'From (estimated)' : 'From'}
|
||||
value={formattedAmounts[Field.INPUT]}
|
||||
showMaxButton={!atMaxAmountInput}
|
||||
token={tokens[Field.INPUT]}
|
||||
onUserInput={onUserInput}
|
||||
onMax={() => {
|
||||
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
|
||||
}}
|
||||
onTokenSelection={address => {
|
||||
setApprovalSubmitted(false) // reset 2 step UI for approvals
|
||||
onTokenSelection(Field.INPUT, address)
|
||||
}}
|
||||
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
|
||||
id="swap-currency-input"
|
||||
/>
|
||||
<CurrencyInputPanel
|
||||
field={Field.INPUT}
|
||||
label={independentField === Field.OUTPUT ? 'From (estimated)' : 'From'}
|
||||
value={formattedAmounts[Field.INPUT]}
|
||||
showMaxButton={!atMaxAmountInput}
|
||||
token={tokens[Field.INPUT]}
|
||||
onUserInput={onUserInput}
|
||||
onMax={() => {
|
||||
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
|
||||
}}
|
||||
onTokenSelection={address => {
|
||||
setApprovalSubmitted(false) // reset 2 step UI for approvals
|
||||
onTokenSelection(Field.INPUT, address)
|
||||
}}
|
||||
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
|
||||
id="swap-currency-input"
|
||||
/>
|
||||
|
||||
<CursorPointer>
|
||||
<AutoColumn style={{ padding: '0 1rem' }}>
|
||||
<ArrowWrapper>
|
||||
<CursorPointer>
|
||||
<AutoColumn justify="space-between">
|
||||
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
|
||||
<ArrowWrapper clickable>
|
||||
<ArrowDown
|
||||
size="16"
|
||||
onClick={() => {
|
||||
@ -266,20 +273,39 @@ export default function Swap() {
|
||||
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 ? 'To (estimated)' : 'To'}
|
||||
showMaxButton={false}
|
||||
token={tokens[Field.OUTPUT]}
|
||||
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
|
||||
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
|
||||
id="swap-currency-output"
|
||||
/>
|
||||
</>
|
||||
{recipient === null ? (
|
||||
<LinkStyledButton id="add-recipient-button" onClick={() => onChangeRecipient('')}>
|
||||
+ add recipient
|
||||
</LinkStyledButton>
|
||||
) : null}
|
||||
</AutoRow>
|
||||
</AutoColumn>
|
||||
</CursorPointer>
|
||||
<CurrencyInputPanel
|
||||
field={Field.OUTPUT}
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
onUserInput={onUserInput}
|
||||
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
|
||||
showMaxButton={false}
|
||||
token={tokens[Field.OUTPUT]}
|
||||
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
|
||||
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
|
||||
id="swap-currency-output"
|
||||
/>
|
||||
|
||||
{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'}>
|
||||
<AutoColumn gap="4px">
|
||||
@ -290,7 +316,7 @@ export default function Swap() {
|
||||
<TradePrice
|
||||
inputToken={tokens[Field.INPUT]}
|
||||
outputToken={tokens[Field.OUTPUT]}
|
||||
price={bestTrade?.executionPrice}
|
||||
price={trade?.executionPrice}
|
||||
showInverted={showInverted}
|
||||
setShowInverted={setShowInverted}
|
||||
/>
|
||||
@ -371,7 +397,7 @@ export default function Swap() {
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
<AdvancedSwapDetailsDropdown trade={bestTrade} />
|
||||
<AdvancedSwapDetailsDropdown trade={trade} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -13,4 +13,6 @@ export const replaceSwapState = createAction<{
|
||||
typedValue: string
|
||||
inputTokenAddress?: string
|
||||
outputTokenAddress?: string
|
||||
recipient: string | null
|
||||
}>('replaceSwapState')
|
||||
export const setRecipient = createAction<{ recipient: string | null }>('setRecipient')
|
||||
|
@ -18,7 +18,8 @@ describe('hooks', () => {
|
||||
[Field.OUTPUT]: { address: '0x6B175474E89094C44Da98b954EedeAC495271d0F' },
|
||||
[Field.INPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||
typedValue: '20.5',
|
||||
independentField: Field.OUTPUT
|
||||
independentField: Field.OUTPUT,
|
||||
recipient: null
|
||||
})
|
||||
})
|
||||
|
||||
@ -32,7 +33,8 @@ describe('hooks', () => {
|
||||
[Field.INPUT]: { address: '' },
|
||||
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||
typedValue: '',
|
||||
independentField: Field.INPUT
|
||||
independentField: Field.INPUT,
|
||||
recipient: null
|
||||
})
|
||||
})
|
||||
|
||||
@ -46,7 +48,58 @@ describe('hooks', () => {
|
||||
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||
[Field.INPUT]: { address: '' },
|
||||
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 { ChainId, JSBI, Token, TokenAmount, Trade, WETH } from '@uniswap/sdk'
|
||||
import { ParsedQs } from 'qs'
|
||||
@ -12,7 +13,7 @@ import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import { isAddress } from '../../utils'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
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 useToggledVersion from '../../hooks/useToggledVersion'
|
||||
import { useUserSlippageTolerance } from '../user/hooks'
|
||||
@ -26,6 +27,7 @@ export function useSwapActionHandlers(): {
|
||||
onTokenSelection: (field: Field, address: string) => void
|
||||
onSwitchTokens: () => void
|
||||
onUserInput: (field: Field, typedValue: string) => void
|
||||
onChangeRecipient: (recipient: string | null) => void
|
||||
} {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const onTokenSelection = useCallback(
|
||||
@ -51,10 +53,18 @@ export function useSwapActionHandlers(): {
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const onChangeRecipient = useCallback(
|
||||
(recipient: string | null) => {
|
||||
dispatch(setRecipient({ recipient }))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return {
|
||||
onSwitchTokens,
|
||||
onTokenSelection,
|
||||
onUserInput
|
||||
onUserInput,
|
||||
onChangeRecipient
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,11 +103,14 @@ export function useDerivedSwapInfo(): {
|
||||
independentField,
|
||||
typedValue,
|
||||
[Field.INPUT]: { address: tokenInAddress },
|
||||
[Field.OUTPUT]: { address: tokenOutAddress }
|
||||
[Field.OUTPUT]: { address: tokenOutAddress },
|
||||
recipient
|
||||
} = useSwapState()
|
||||
|
||||
const tokenIn = useToken(tokenInAddress)
|
||||
const tokenOut = useToken(tokenOutAddress)
|
||||
const recipientLookup = useENS(recipient ?? undefined)
|
||||
const to: string | null = (recipient === null ? account : recipientLookup.address) ?? null
|
||||
|
||||
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [
|
||||
tokenIn ?? undefined,
|
||||
@ -138,6 +151,10 @@ export function useDerivedSwapInfo(): {
|
||||
error = error ?? 'Select a token'
|
||||
}
|
||||
|
||||
if (!to) {
|
||||
error = error ?? 'Enter a recipient'
|
||||
}
|
||||
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
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') {
|
||||
const valid = isAddress(urlParam)
|
||||
if (valid) return valid
|
||||
if (urlParam.toLowerCase() === 'eth') 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 {
|
||||
@ -190,9 +207,20 @@ function parseIndependentFieldURLParameter(urlParam: any): Field {
|
||||
return typeof urlParam === 'string' && urlParam.toLowerCase() === 'output' ? Field.OUTPUT : Field.INPUT
|
||||
}
|
||||
|
||||
export function queryParametersToSwapState(parsedQs: ParsedQs, chainId: ChainId, overrideETH: boolean): SwapState {
|
||||
let inputCurrency = parseCurrencyFromURLParameter(parsedQs.inputCurrency, chainId, overrideETH)
|
||||
let outputCurrency = parseCurrencyFromURLParameter(parsedQs.outputCurrency, chainId, overrideETH)
|
||||
const ENS_NAME_REGEX = /^[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)?$/
|
||||
const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/
|
||||
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 (typeof parsedQs.outputCurrency === 'string') {
|
||||
inputCurrency = ''
|
||||
@ -201,6 +229,8 @@ export function queryParametersToSwapState(parsedQs: ParsedQs, chainId: ChainId,
|
||||
}
|
||||
}
|
||||
|
||||
const recipient = validatedRecipient(parsedQs.recipient)
|
||||
|
||||
return {
|
||||
[Field.INPUT]: {
|
||||
address: inputCurrency
|
||||
@ -209,29 +239,29 @@ export function queryParametersToSwapState(parsedQs: ParsedQs, chainId: ChainId,
|
||||
address: outputCurrency
|
||||
},
|
||||
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
|
||||
// set overrideETH to true if dont want to autopopulate ETH
|
||||
export function useDefaultsFromURLSearch(overrideWETH = false) {
|
||||
export function useDefaultsFromURLSearch() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const parsedQs = useParsedQueryString()
|
||||
|
||||
useEffect(() => {
|
||||
if (!chainId) return
|
||||
const parsed = queryParametersToSwapState(parsedQs, chainId, overrideWETH)
|
||||
const parsed = queryParametersToSwapState(parsedQs, chainId)
|
||||
|
||||
dispatch(
|
||||
replaceSwapState({
|
||||
typedValue: parsed.typedValue,
|
||||
field: parsed.independentField,
|
||||
inputTokenAddress: parsed[Field.INPUT].address,
|
||||
outputTokenAddress: parsed[Field.OUTPUT].address
|
||||
outputTokenAddress: parsed[Field.OUTPUT].address,
|
||||
recipient: parsed.recipient
|
||||
})
|
||||
)
|
||||
// eslint-disable-next-line
|
||||
}, [dispatch, chainId])
|
||||
}, [dispatch, chainId, parsedQs])
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
readonly independentField: Field
|
||||
@ -10,6 +10,8 @@ export interface SwapState {
|
||||
readonly [Field.OUTPUT]: {
|
||||
readonly address: string | undefined
|
||||
}
|
||||
// the typed recipient address, or null if swap should go to sender
|
||||
readonly recipient: string | null
|
||||
}
|
||||
|
||||
const initialState: SwapState = {
|
||||
@ -20,23 +22,28 @@ const initialState: SwapState = {
|
||||
},
|
||||
[Field.OUTPUT]: {
|
||||
address: ''
|
||||
}
|
||||
},
|
||||
recipient: null
|
||||
}
|
||||
|
||||
export default createReducer<SwapState>(initialState, builder =>
|
||||
builder
|
||||
.addCase(replaceSwapState, (state, { payload: { typedValue, field, inputTokenAddress, outputTokenAddress } }) => {
|
||||
return {
|
||||
[Field.INPUT]: {
|
||||
address: inputTokenAddress
|
||||
},
|
||||
[Field.OUTPUT]: {
|
||||
address: outputTokenAddress
|
||||
},
|
||||
independentField: field,
|
||||
typedValue: typedValue
|
||||
.addCase(
|
||||
replaceSwapState,
|
||||
(state, { payload: { typedValue, recipient, field, inputTokenAddress, outputTokenAddress } }) => {
|
||||
return {
|
||||
[Field.INPUT]: {
|
||||
address: inputTokenAddress
|
||||
},
|
||||
[Field.OUTPUT]: {
|
||||
address: outputTokenAddress
|
||||
},
|
||||
independentField: field,
|
||||
typedValue: typedValue,
|
||||
recipient
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.addCase(selectToken, (state, { payload: { address, field } }) => {
|
||||
const otherField = field === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
if (address === state[otherField].address) {
|
||||
@ -70,4 +77,7 @@ export default createReducer<SwapState>(initialState, builder =>
|
||||
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)
|
||||
|
||||
return state[chainId ?? -1] ?? {}
|
||||
return chainId ? state[chainId] ?? {} : {}
|
||||
}
|
||||
|
||||
export function useIsTransactionPending(transactionHash?: string): boolean {
|
||||
|
@ -188,6 +188,10 @@ body {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
|
Loading…
Reference in New Issue
Block a user