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:
Moody Salem 2020-07-08 23:06:29 -04:00 committed by GitHub
parent 8a845ee0e9
commit 21c1484c0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 466 additions and 1059 deletions

@ -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

@ -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
}
}

@ -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;