update swr hooks when block number changes (#771)

* key per-block SWR data with block number

* fix + tweak copy and decrease sig figs for lp fee

* don't use blocknumber as a key

instead mutate when it changes

* fix totalsupply + user liquidity sync issues

* fix v1 trade checker logic

* rough fix allowing removal of user added tokens

probably needs a more comprehensive overhaul...

* refactor SWR block updates to custom hook

* typo

* fix import error

* fix footer css to work cross-broswer

* disallow \ to be typing into amount inputs

add test case for this

add value assertions to all input integration tests

* fix import error

* remove console.log

* clean up address input a bit

trim whitespace from pasted addresses

* fix input maxing

remove spurious max output logic
This commit is contained in:
Noah Zinsmeister 2020-05-14 17:12:59 -04:00 committed by GitHub
parent b1ffab1890
commit 7bffea0692
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 224 additions and 253 deletions

@ -2,6 +2,8 @@ describe('Send', () => {
beforeEach(() => cy.visit('/send')) beforeEach(() => cy.visit('/send'))
it('can enter an amount into input', () => { it('can enter an amount into input', () => {
cy.get('#sending-no-swap-input').type('0.001') cy.get('#sending-no-swap-input')
.type('0.001')
.should('have.value', '0.001')
}) })
}) })

@ -1,19 +1,33 @@
describe('Swap', () => { describe('Swap', () => {
beforeEach(() => cy.visit('/swap')) beforeEach(() => cy.visit('/swap'))
it('can enter an amount into input', () => { it('can enter an amount into input', () => {
cy.get('#swap-currency-input .token-amount-input').type('0.001') cy.get('#swap-currency-input .token-amount-input')
.type('0.001')
.should('have.value', '0.001')
}) })
it('zero swap amount', () => { it('zero swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').type('0.0') cy.get('#swap-currency-input .token-amount-input')
.type('0.0')
.should('have.value', '0.0')
})
it('invalid swap amount', () => {
cy.get('#swap-currency-input .token-amount-input')
.type('\\')
.should('have.value', '')
}) })
it('can enter an amount into output', () => { it('can enter an amount into output', () => {
cy.get('#swap-currency-output .token-amount-input').type('0.001') cy.get('#swap-currency-output .token-amount-input')
.type('0.001')
.should('have.value', '0.001')
}) })
it('zero output amount', () => { it('zero output amount', () => {
cy.get('#swap-currency-output .token-amount-input').type('0.0') cy.get('#swap-currency-output .token-amount-input')
.type('0.0')
.should('have.value', '0.0')
}) })
it('can swap ETH for DAI', () => { it('can swap ETH for DAI', () => {

@ -76,17 +76,15 @@ export default function AddressInputPanel({
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const [input, setInput] = useState(initialInput ? initialInput : '') const [input, setInput] = useState(initialInput ? initialInput : '')
const debouncedInput = useDebounce(input, 200)
const debouncedInput = useDebounce(input, 150) const [data, setData] = useState<{ address: string; name: string }>({ address: undefined, name: undefined })
const [data, setData] = useState({ address: undefined, name: undefined })
const [error, setError] = useState<boolean>(false) const [error, setError] = useState<boolean>(false)
// keep data and errors in sync // keep data and errors in sync
useEffect(() => { useEffect(() => {
onChange({ address: data.address, name: data.name }) onChange({ address: data.address, name: data.name })
}, [onChange, data.address, data.name]) }, [onChange, data.address, data.name])
useEffect(() => { useEffect(() => {
onError(error, input) onError(error, input)
}, [onError, error, input]) }, [onError, error, input])
@ -94,55 +92,45 @@ export default function AddressInputPanel({
// run parser on debounced input // run parser on debounced input
useEffect(() => { useEffect(() => {
let stale = false let stale = false
// if the input is an address, try to look up its name
if (isAddress(debouncedInput)) { if (isAddress(debouncedInput)) {
try { 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 library
.lookupAddress(debouncedInput) .resolveName(debouncedInput)
.then(name => { .then(address => {
if (!stale) { if (stale) return
// if an ENS name exists, set it as the destination // if the debounced input name resolves to an address
if (name) { if (address) {
setInput(name) setData({ address: address, name: debouncedInput })
} else { setError(null)
setData({ address: debouncedInput, name: '' }) } else {
setError(null) setError(true)
}
} }
}) })
.catch(() => { .catch(() => {
if (!stale) { if (stale) return
setData({ address: debouncedInput, name: '' }) setError(true)
setError(null)
}
}) })
} catch {
setData({ address: debouncedInput, name: '' })
setError(null)
}
} else {
if (debouncedInput !== '') {
try {
library
.resolveName(debouncedInput)
.then(address => {
if (!stale) {
// if the debounced input name resolves to an address
if (address) {
setData({ address: address, name: debouncedInput })
setError(null)
} else {
setError(true)
}
}
})
.catch(() => {
if (!stale) {
setError(true)
}
})
} catch {
setError(true)
}
} else if (debouncedInput === '') { } else if (debouncedInput === '') {
setError(true) setError(true)
} }
@ -151,22 +139,13 @@ export default function AddressInputPanel({
return () => { return () => {
stale = true stale = true
} }
}, [debouncedInput, library, onChange, onError]) }, [debouncedInput, library])
function onInput(event) { function onInput(event) {
if (event.target.value === '') { setData({ address: undefined, name: undefined })
setData({ address: undefined, name: undefined }) setError(false)
}
if (data.address !== undefined || data.name !== undefined) {
setData({ address: undefined, name: undefined })
}
if (error !== undefined) {
setError(true)
}
const input = event.target.value const input = event.target.value
const checksummedInput = isAddress(input) const checksummedInput = isAddress(input.replace(/\s/g, '')) // delete whitespace
setInput(checksummedInput || input) setInput(checksummedInput || input)
} }

@ -1,7 +1,7 @@
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint256 } from '@ethersproject/constants' import { MaxUint256 } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { parseEther, parseUnits } from '@ethersproject/units' import { parseUnits } from '@ethersproject/units'
import { Fraction, JSBI, Percent, TokenAmount, TradeType, WETH } from '@uniswap/sdk' import { Fraction, JSBI, Percent, TokenAmount, TradeType, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useState } from 'react' import React, { useCallback, useContext, useEffect, useState } from 'react'
import { ArrowDown, ChevronDown, ChevronUp, Repeat } from 'react-feather' import { ArrowDown, ChevronDown, ChevronUp, Repeat } from 'react-feather'
@ -74,6 +74,9 @@ const DEFAULT_DEADLINE_FROM_NOW = 60 * 20
const ALLOWED_SLIPPAGE_MEDIUM = 100 const ALLOWED_SLIPPAGE_MEDIUM = 100
const ALLOWED_SLIPPAGE_HIGH = 500 const ALLOWED_SLIPPAGE_HIGH = 500
// used to ensure the user doesn't send so much ETH so they end up with <.01
const MIN_ETH: JSBI = JSBI.BigInt(`1${'0'.repeat(16)}`) // .01 ETH
interface ExchangePageProps extends RouteComponentProps { interface ExchangePageProps extends RouteComponentProps {
sendingInput: boolean sendingInput: boolean
params: QueryParams params: QueryParams
@ -130,8 +133,8 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
// get user- and token-specific lookup data // get user- and token-specific lookup data
const userBalances = { const userBalances = {
[Field.INPUT]: allBalances?.[tokens[Field.INPUT]?.address]?.raw, [Field.INPUT]: allBalances?.[account]?.[tokens[Field.INPUT]?.address],
[Field.OUTPUT]: allBalances?.[tokens[Field.OUTPUT]?.address]?.raw [Field.OUTPUT]: allBalances?.[account]?.[tokens[Field.OUTPUT]?.address]
} }
// parse the amount that the user typed // parse the amount that the user typed
@ -251,19 +254,6 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
[dispatch] [dispatch]
) )
const onMaxOutput = useCallback(
(typedValue: string) => {
dispatch({
type: SwapAction.TYPE,
payload: {
field: Field.OUTPUT,
typedValue
}
})
},
[dispatch]
)
// reset field if sending with with swap is cancled // reset field if sending with with swap is cancled
useEffect(() => { useEffect(() => {
if (sending && !sendingWithSwap) { if (sending && !sendingWithSwap) {
@ -271,39 +261,19 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
} }
}, [onTokenSelection, sending, sendingWithSwap]) }, [onTokenSelection, sending, sendingWithSwap])
const MIN_ETHER: TokenAmount = chainId && new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01'))) const maxAmountInput: TokenAmount =
!!userBalances[Field.INPUT] &&
let maxAmountInput: TokenAmount !!tokens[Field.INPUT] &&
!!WETH[chainId] &&
try { userBalances[Field.INPUT].greaterThan(
maxAmountInput = new TokenAmount(tokens[Field.INPUT], tokens[Field.INPUT].equals(WETH[chainId]) ? MIN_ETH : '0')
!!userBalances[Field.INPUT] && )
!!tokens[Field.INPUT] && ? tokens[Field.INPUT].equals(WETH[chainId])
WETH[chainId] && ? userBalances[Field.INPUT].subtract(new TokenAmount(WETH[chainId], MIN_ETH))
JSBI.greaterThan( : userBalances[Field.INPUT]
userBalances[Field.INPUT].raw, : undefined
tokens[Field.INPUT].equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
)
? tokens[Field.INPUT].equals(WETH[chainId])
? userBalances[Field.INPUT].subtract(MIN_ETHER)
: userBalances[Field.INPUT]
: undefined
} catch {}
const atMaxAmountInput: boolean = const atMaxAmountInput: boolean =
!!maxAmountInput && !!parsedAmounts[Field.INPUT] !!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
? JSBI.equal(maxAmountInput.raw, parsedAmounts[Field.INPUT].raw)
: undefined
const maxAmountOutput: TokenAmount =
!!userBalances[Field.OUTPUT] && JSBI.greaterThan(userBalances[Field.OUTPUT].raw, JSBI.BigInt(0))
? userBalances[Field.OUTPUT]
: undefined
const atMaxAmountOutput: boolean =
!!maxAmountOutput && !!parsedAmounts[Field.OUTPUT]
? JSBI.equal(maxAmountOutput.raw, parsedAmounts[Field.OUTPUT].raw)
: undefined
function getSwapType(): SwapType { function getSwapType(): SwapType {
if (tradeType === TradeType.EXACT_INPUT) { if (tradeType === TradeType.EXACT_INPUT) {
@ -528,11 +498,11 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
const estimatedGas = await tokenContract.estimateGas.approve(ROUTER_ADDRESS, MaxUint256).catch(() => { const estimatedGas = await tokenContract.estimateGas.approve(ROUTER_ADDRESS, MaxUint256).catch(() => {
// general fallback for tokens who restrict approval amounts // general fallback for tokens who restrict approval amounts
useUserBalance = true useUserBalance = true
return tokenContract.estimateGas.approve(ROUTER_ADDRESS, userBalances[field]) return tokenContract.estimateGas.approve(ROUTER_ADDRESS, userBalances[field].raw.toString())
}) })
tokenContract tokenContract
.approve(ROUTER_ADDRESS, useUserBalance ? userBalances[field] : MaxUint256, { .approve(ROUTER_ADDRESS, useUserBalance ? userBalances[field].raw.toString() : MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas) gasLimit: calculateGasMargin(estimatedGas)
}) })
.then(response => { .then(response => {
@ -585,7 +555,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
if ( if (
userBalances[Field.INPUT] && userBalances[Field.INPUT] &&
parsedAmounts[Field.INPUT] && parsedAmounts[Field.INPUT] &&
JSBI.lessThan(userBalances[Field.INPUT].raw, parsedAmounts[Field.INPUT]?.raw) userBalances[Field.INPUT].lessThan(parsedAmounts[Field.INPUT])
) { ) {
setInputError('Insufficient ' + tokens[Field.INPUT]?.symbol + ' balance') setInputError('Insufficient ' + tokens[Field.INPUT]?.symbol + ' balance')
setIsValid(false) setIsValid(false)
@ -905,9 +875,8 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}` ]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
function _onTokenSelect(address: string) { function _onTokenSelect(address: string) {
const balance = allBalances?.[account]?.[address]
// if no user balance - switch view to a send with swap // if no user balance - switch view to a send with swap
const hasBalance = balance && JSBI.greaterThan(balance.raw, JSBI.BigInt(0)) const hasBalance = allBalances?.[account]?.[address]?.greaterThan('0')
if (!hasBalance && sending) { if (!hasBalance && sending) {
onTokenSelection(Field.INPUT, null) onTokenSelection(Field.INPUT, null)
onTokenSelection(Field.OUTPUT, address) onTokenSelection(Field.OUTPUT, address)
@ -1047,11 +1016,10 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
field={Field.OUTPUT} field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]} value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput} onUserInput={onUserInput}
onMax={() => { // eslint-disable-next-line @typescript-eslint/no-empty-function
maxAmountOutput && onMaxOutput(maxAmountOutput.toExact()) onMax={() => {}}
}} atMax={true}
label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'} label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'}
atMax={atMaxAmountOutput}
token={tokens[Field.OUTPUT]} token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)} onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
advanced={advanced} advanced={advanced}
@ -1286,11 +1254,11 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Liquidity Provider Fee Liquidity Provider Fee
</TYPE.black> </TYPE.black>
<QuestionHelper text="A portion of each trade (0.03%) goes to liquidity providers to incentivize liquidity on the protocol." /> <QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed> </RowFixed>
<TYPE.black fontSize={14} color={theme.text1}> <TYPE.black fontSize={14} color={theme.text1}>
{realizedLPFeeAmount {realizedLPFeeAmount
? realizedLPFeeAmount?.toSignificant(6) + ' ' + tokens[Field.INPUT]?.symbol ? realizedLPFeeAmount?.toSignificant(4) + ' ' + tokens[Field.INPUT]?.symbol
: '-'} : '-'}
</TYPE.black> </TYPE.black>
</RowBetween> </RowBetween>

@ -9,7 +9,6 @@ const FooterFrame = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
width: 100%;
position: fixed; position: fixed;
right: 1rem; right: 1rem;
bottom: 1rem; bottom: 1rem;
@ -24,24 +23,11 @@ export default function Footer() {
return ( return (
<FooterFrame> <FooterFrame>
<form action="https://forms.gle/DaLuqvJsVhVaAM3J9" target="_blank"> <form action="https://forms.gle/DaLuqvJsVhVaAM3J9" target="_blank">
<ButtonSecondary <ButtonSecondary p="8px 12px">
style={{
padding: ' 8px 12px',
marginRight: '8px',
width: 'fit-content'
}}
>
<Send size={16} style={{ marginRight: '8px' }} /> Feedback <Send size={16} style={{ marginRight: '8px' }} /> Feedback
</ButtonSecondary> </ButtonSecondary>
</form> </form>
<ButtonSecondary <ButtonSecondary onClick={toggleDarkMode} p="8px 12px" ml="0.5rem" width="min-content">
onClick={toggleDarkMode}
style={{
padding: ' 8px 12px',
marginRight: '0px',
width: 'fit-content'
}}
>
{darkMode ? <Sun size={16} /> : <Moon size={16} />} {darkMode ? <Sun size={16} /> : <Moon size={16} />}
</ButtonSecondary> </ButtonSecondary>
</FooterFrame> </FooterFrame>

@ -40,7 +40,7 @@ const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: s
} }
` `
const inputRegex = RegExp(`^\\d*(?:\\\\.)?\\d*$`) // match escaped "." characters via in a non-capturing group const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group
export const Input = React.memo(function InnerInput({ export const Input = React.memo(function InnerInput({
value, value,

@ -2,7 +2,7 @@ import React, { useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { darken } from 'polished' import { darken } from 'polished'
import { RouteComponentProps, withRouter } from 'react-router-dom' import { RouteComponentProps, withRouter } from 'react-router-dom'
import { Percent, Pair } from '@uniswap/sdk' import { Percent, Pair, JSBI } from '@uniswap/sdk'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { useTotalSupply } from '../../data/TotalSupply' import { useTotalSupply } from '../../data/TotalSupply'
@ -47,23 +47,21 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
const totalPoolTokens = useTotalSupply(pair?.liquidityToken) const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
const poolTokenPercentage = const poolTokenPercentage =
!!userPoolBalance && !!totalPoolTokens ? new Percent(userPoolBalance.raw, totalPoolTokens.raw) : undefined !!userPoolBalance && !!totalPoolTokens && JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
? new Percent(userPoolBalance.raw, totalPoolTokens.raw)
: undefined
const token0Deposited = const [token0Deposited, token1Deposited] =
token0 && !!pair &&
totalPoolTokens && !!totalPoolTokens &&
userPoolBalance && !!userPoolBalance &&
pair && // this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
totalPoolTokens && JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
pair.liquidityToken.equals(totalPoolTokens.token) && ? [
pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false) pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false),
const token1Deposited = pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false)
token1 && ]
totalPoolTokens && : [undefined, undefined]
userPoolBalance &&
totalPoolTokens &&
pair.liquidityToken.equals(totalPoolTokens.token) &&
pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false)
if (minimal) { if (minimal) {
return ( return (
@ -87,7 +85,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
</RowFixed> </RowFixed>
<RowFixed> <RowFixed>
<Text fontWeight={500} fontSize={20}> <Text fontWeight={500} fontSize={20}>
{userPoolBalance ? userPoolBalance.toSignificant(5) : '-'} {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
</Text> </Text>
</RowFixed> </RowFixed>
</FixedHeightRow> </FixedHeightRow>

@ -28,10 +28,11 @@ import {
useAllDummyPairs, useAllDummyPairs,
useFetchTokenByAddress, useFetchTokenByAddress,
useAddUserToken, useAddUserToken,
useRemoveUserAddedToken useRemoveUserAddedToken,
useUserAddedTokens
} from '../../state/user/hooks' } from '../../state/user/hooks'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useToken, useAllTokens, ALL_TOKENS } from '../../hooks/Tokens' import { useToken, useAllTokens } from '../../hooks/Tokens'
import QuestionHelper from '../Question' import QuestionHelper from '../Question'
const TokenModalInfo = styled.div` const TokenModalInfo = styled.div`
@ -183,6 +184,7 @@ function SearchModal({
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [sortDirection, setSortDirection] = useState(true) const [sortDirection, setSortDirection] = useState(true)
const userAddedTokens = useUserAddedTokens()
const fetchTokenByAddress = useFetchTokenByAddress() const fetchTokenByAddress = useFetchTokenByAddress()
const addToken = useAddUserToken() const addToken = useAddUserToken()
const removeTokenByAddress = useRemoveUserAddedToken() const removeTokenByAddress = useRemoveUserAddedToken()
@ -259,12 +261,8 @@ function SearchModal({
const filteredTokenList = useMemo(() => { const filteredTokenList = useMemo(() => {
return tokenList.filter(tokenEntry => { return tokenList.filter(tokenEntry => {
const urlAdded = urlAddedTokens && urlAddedTokens.hasOwnProperty(tokenEntry.address) const urlAdded = urlAddedTokens?.some(token => token.address === tokenEntry.address)
const customAdded = const customAdded = userAddedTokens?.some(token => token.address === tokenEntry.address) && !urlAdded
tokenEntry.address !== 'ETH' &&
ALL_TOKENS[chainId] &&
!ALL_TOKENS[chainId].hasOwnProperty(tokenEntry.address) &&
!urlAdded
// if token import page dont show preset list, else show all // if token import page dont show preset list, else show all
const include = !showTokenImport || (showTokenImport && customAdded && searchQuery !== '') const include = !showTokenImport || (showTokenImport && customAdded && searchQuery !== '')
@ -287,7 +285,7 @@ function SearchModal({
}) })
return regexMatches.some(m => m) return regexMatches.some(m => m)
}) })
}, [tokenList, urlAddedTokens, chainId, showTokenImport, searchQuery]) }, [tokenList, urlAddedTokens, userAddedTokens, showTokenImport, searchQuery])
function _onTokenSelect(address) { function _onTokenSelect(address) {
setSearchQuery('') setSearchQuery('')
@ -457,9 +455,8 @@ function SearchModal({
} }
}) })
.map(({ address, symbol, balance }) => { .map(({ address, symbol, balance }) => {
const urlAdded = urlAddedTokens && urlAddedTokens.hasOwnProperty(address) const urlAdded = urlAddedTokens?.some(token => token.address === address)
const customAdded = const customAdded = userAddedTokens?.some(token => token.address === address) && !urlAdded
address !== 'ETH' && ALL_TOKENS[chainId] && !ALL_TOKENS[chainId].hasOwnProperty(address) && !urlAdded
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw) const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
@ -481,7 +478,8 @@ function SearchModal({
</Text> </Text>
<FadedSpan> <FadedSpan>
<TYPE.blue fontWeight={500}> <TYPE.blue fontWeight={500}>
{urlAdded && '(Added by URL)'} {customAdded && 'Added by user'} {urlAdded && 'Added by URL'}
{customAdded && 'Added by user'}
</TYPE.blue> </TYPE.blue>
{customAdded && ( {customAdded && (
<div <div

@ -2,14 +2,11 @@ import { Contract } from '@ethersproject/contracts'
import { Token, TokenAmount } from '@uniswap/sdk' import { Token, TokenAmount } from '@uniswap/sdk'
import useSWR from 'swr' import useSWR from 'swr'
import { SWRKeys } from '.' import { SWRKeys, useKeepSWRDataLiveAsBlocksArrive } from '.'
import { useTokenContract } from '../hooks' import { useTokenContract } from '../hooks'
function getTokenAllowance( function getTokenAllowance(contract: Contract, token: Token): (owner: string, spender: string) => Promise<TokenAmount> {
contract: Contract, return async (owner: string, spender: string): Promise<TokenAmount> =>
token: Token
): (_: SWRKeys, __: number, ___: string, owner: string, spender: string) => Promise<TokenAmount> {
return async (_, __, ___, owner: string, spender: string): Promise<TokenAmount> =>
contract contract
.allowance(owner, spender) .allowance(owner, spender)
.then((balance: { toString: () => string }) => new TokenAmount(token, balance.toString())) .then((balance: { toString: () => string }) => new TokenAmount(token, balance.toString()))
@ -17,16 +14,13 @@ function getTokenAllowance(
export function useTokenAllowance(token?: Token, owner?: string, spender?: string): TokenAmount { export function useTokenAllowance(token?: Token, owner?: string, spender?: string): TokenAmount {
const contract = useTokenContract(token?.address, false) const contract = useTokenContract(token?.address, false)
const shouldFetch = !!contract && typeof owner === 'string' && typeof spender === 'string'
const { data } = useSWR( const shouldFetch = !!contract && typeof owner === 'string' && typeof spender === 'string'
shouldFetch ? [SWRKeys.Allowances, token.chainId, token.address, owner, spender] : null, const { data, mutate } = useSWR(
getTokenAllowance(contract, token), shouldFetch ? [owner, spender, token.address, token.chainId, SWRKeys.Allowances] : null,
{ getTokenAllowance(contract, token)
dedupingInterval: 10 * 1000,
refreshInterval: 20 * 1000
}
) )
useKeepSWRDataLiveAsBlocksArrive(mutate)
return data return data
} }

@ -3,15 +3,16 @@ import { Token, TokenAmount, Pair } from '@uniswap/sdk'
import useSWR from 'swr' import useSWR from 'swr'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json' import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { SWRKeys, useKeepSWRDataLiveAsBlocksArrive } from '.'
import { useContract } from '../hooks' import { useContract } from '../hooks'
import { SWRKeys } from '.'
function getReserves(contract: Contract, token0: Token, token1: Token): () => Promise<Pair | null> { function getReserves(contract: Contract, tokenA: Token, tokenB: Token): () => Promise<Pair | null> {
return async (): Promise<Pair | null> => return async (): Promise<Pair | null> =>
contract contract
.getReserves() .getReserves()
.then( .then(
({ reserve0, reserve1 }: { reserve0: { toString: () => string }; reserve1: { toString: () => string } }) => { ({ reserve0, reserve1 }: { reserve0: { toString: () => string }; reserve1: { toString: () => string } }) => {
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString())) return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
} }
) )
@ -26,21 +27,13 @@ function getReserves(contract: Contract, token0: Token, token1: Token): () => Pr
* if pair already created (even if 0 reserves), return pair * if pair already created (even if 0 reserves), return pair
*/ */
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null { export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
const bothDefined = !!tokenA && !!tokenB const pairAddress = !!tokenA && !!tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
const invalid = bothDefined && tokenA.equals(tokenB)
const [token0, token1] =
bothDefined && !invalid ? (tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]) : []
const pairAddress = !!token0 && !!token1 ? Pair.getAddress(token0, token1) : undefined
const contract = useContract(pairAddress, IUniswapV2PairABI, false) const contract = useContract(pairAddress, IUniswapV2PairABI, false)
const shouldFetch = !!contract const shouldFetch = !!contract
const { data } = useSWR( const key = shouldFetch ? [pairAddress, tokenA.chainId, SWRKeys.Reserves] : null
shouldFetch ? [SWRKeys.Reserves, token0.chainId, pairAddress] : null, const { data, mutate } = useSWR(key, getReserves(contract, tokenA, tokenB))
getReserves(contract, token0, token1), useKeepSWRDataLiveAsBlocksArrive(mutate)
{
dedupingInterval: 10 * 1000,
refreshInterval: 20 * 1000
}
)
return data return data
} }

@ -3,8 +3,8 @@ import { Token, TokenAmount } from '@uniswap/sdk'
import useSWR from 'swr' import useSWR from 'swr'
import { abi as IERC20ABI } from '@uniswap/v2-core/build/IERC20.json' import { abi as IERC20ABI } from '@uniswap/v2-core/build/IERC20.json'
import { SWRKeys, useKeepSWRDataLiveAsBlocksArrive } from '.'
import { useContract } from '../hooks' import { useContract } from '../hooks'
import { SWRKeys } from '.'
function getTotalSupply(contract: Contract, token: Token): () => Promise<TokenAmount> { function getTotalSupply(contract: Contract, token: Token): () => Promise<TokenAmount> {
return async (): Promise<TokenAmount> => return async (): Promise<TokenAmount> =>
@ -15,14 +15,13 @@ function getTotalSupply(contract: Contract, token: Token): () => Promise<TokenAm
export function useTotalSupply(token?: Token): TokenAmount { export function useTotalSupply(token?: Token): TokenAmount {
const contract = useContract(token?.address, IERC20ABI, false) const contract = useContract(token?.address, IERC20ABI, false)
const shouldFetch = !!contract const shouldFetch = !!contract
const { data } = useSWR( const { data, mutate } = useSWR(
shouldFetch ? [SWRKeys.TotalSupply, token.chainId, token.address] : null, shouldFetch ? [token.address, token.chainId, SWRKeys.TotalSupply] : null,
getTotalSupply(contract, token), getTotalSupply(contract, token)
{
dedupingInterval: 10 * 1000,
refreshInterval: 20 * 1000
}
) )
useKeepSWRDataLiveAsBlocksArrive(mutate)
return data return data
} }

@ -1,6 +1,7 @@
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { Token, TokenAmount, Pair, Trade, ChainId, WETH, Route, TradeType, Percent } from '@uniswap/sdk' import { Token, TokenAmount, Pair, Trade, ChainId, WETH, Route, TradeType, Percent } from '@uniswap/sdk'
import useSWR from 'swr' import useSWR from 'swr'
import { useWeb3React } from '@web3-react/core'
import IUniswapV1Factory from '../constants/abis/v1_factory.json' import IUniswapV1Factory from '../constants/abis/v1_factory.json'
import { V1_FACTORY_ADDRESS } from '../constants' import { V1_FACTORY_ADDRESS } from '../constants'
@ -8,31 +9,35 @@ import { useContract } from '../hooks'
import { SWRKeys } from '.' import { SWRKeys } from '.'
import { useETHBalances, useTokenBalances } from '../state/wallet/hooks' import { useETHBalances, useTokenBalances } from '../state/wallet/hooks'
function getV1PairAddress(contract: Contract): (_: SWRKeys, tokenAddress: string) => Promise<string> { function getV1PairAddress(contract: Contract): (tokenAddress: string) => Promise<string> {
return async (_: SWRKeys, tokenAddress: string): Promise<string> => contract.getExchange(tokenAddress) return async (tokenAddress: string): Promise<string> => contract.getExchange(tokenAddress)
} }
function useV1PairAddress(tokenAddress: string) { function useV1PairAddress(tokenAddress: string) {
const contract = useContract(V1_FACTORY_ADDRESS, IUniswapV1Factory, false) const { chainId } = useWeb3React()
const shouldFetch = typeof tokenAddress === 'string' && !!contract
const { data } = useSWR(shouldFetch ? [SWRKeys.V1PairAddress, tokenAddress] : null, getV1PairAddress(contract), { const contract = useContract(V1_FACTORY_ADDRESS, IUniswapV1Factory, false)
refreshInterval: 0 // don't need to update these
const shouldFetch = chainId === ChainId.MAINNET && typeof tokenAddress === 'string' && !!contract
const { data } = useSWR(shouldFetch ? [tokenAddress, SWRKeys.V1PairAddress] : null, getV1PairAddress(contract), {
// don't need to update this data
revalidateOnFocus: false,
revalidateOnReconnect: false
}) })
return data return data
} }
function useMockV1Pair(token?: Token) { function useMockV1Pair(token?: Token) {
const mainnet = token?.chainId === ChainId.MAINNET const isWETH = token?.equals(WETH[token?.chainId])
const isWETH = token?.equals(WETH[ChainId.MAINNET])
const v1PairAddress = useV1PairAddress(mainnet && !isWETH ? token?.address : undefined) // will only return an address on mainnet, and not for WETH
const v1PairAddress = useV1PairAddress(isWETH ? undefined : token?.address)
const tokenBalance = useTokenBalances(v1PairAddress, [token])[token?.address] const tokenBalance = useTokenBalances(v1PairAddress, [token])[token?.address]
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress] const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress]
return tokenBalance && ETHBalance return tokenBalance && ETHBalance
? new Pair(tokenBalance, new TokenAmount(WETH[ChainId.MAINNET], ETHBalance.toString())) ? new Pair(tokenBalance, new TokenAmount(WETH[token?.chainId], ETHBalance.toString()))
: undefined : undefined
} }
@ -63,9 +68,20 @@ export function useV1TradeLinkIfBetter(trade: Trade, minimumDelta: Percent = new
trade.tradeType trade.tradeType
) )
const v1HasBetterRate = v1Trade?.slippage?.add(minimumDelta)?.lessThan(trade?.slippage) let v1HasBetterTrade = false
if (v1Trade) {
if (trade.tradeType === TradeType.EXACT_INPUT) {
// check if the output amount on v1, discounted by minimumDelta, is greater than on v2
const discountedV1Output = v1Trade.outputAmount.multiply(new Percent('1').subtract(minimumDelta))
v1HasBetterTrade = discountedV1Output.greaterThan(trade.outputAmount)
} else {
// check if the input amount on v1, inflated by minimumDelta, is less than on v2
const inflatedV1Input = v1Trade.inputAmount.multiply(new Percent('1').add(minimumDelta))
v1HasBetterTrade = inflatedV1Input.lessThan(trade.inputAmount)
}
}
return v1HasBetterRate return v1HasBetterTrade
? `https://v1.uniswap.exchange/swap?inputCurrency=${ ? `https://v1.uniswap.exchange/swap?inputCurrency=${
inputIsWETH ? 'ETH' : trade.route.input.address inputIsWETH ? 'ETH' : trade.route.input.address
}&outputCurrency=${outputIsWETH ? 'ETH' : trade.route.output.address}` }&outputCurrency=${outputIsWETH ? 'ETH' : trade.route.output.address}`

@ -1,6 +1,24 @@
import { useEffect, useRef } from 'react'
import { responseInterface } from 'swr'
import { useBlockNumber } from '../state/application/hooks'
export enum SWRKeys { export enum SWRKeys {
Allowances, Allowances,
Reserves, Reserves,
TotalSupply, TotalSupply,
V1PairAddress V1PairAddress
} }
export function useKeepSWRDataLiveAsBlocksArrive(mutate: responseInterface<any, any>['mutate']) {
// because we don't care about the referential identity of mutate, just bind it to a ref
const mutateRef = useRef(mutate)
useEffect(() => {
mutateRef.current = mutate
})
// then, whenever a new block arrives, trigger a mutation
const blockNumber = useBlockNumber()
useEffect(() => {
mutateRef.current()
}, [blockNumber])
}

@ -56,7 +56,7 @@ export function useAllTokens(): { [address: string]: Token } {
.reduce<{ [address: string]: Token }>((tokenMap, token) => { .reduce<{ [address: string]: Token }>((tokenMap, token) => {
tokenMap[token.address] = token tokenMap[token.address] = token
return tokenMap return tokenMap
}, ALL_TOKENS?.[chainId] ?? {}) }, ALL_TOKENS[chainId] ?? {})
) )
}, [userAddedTokens, chainId]) }, [userAddedTokens, chainId])
} }

@ -275,17 +275,12 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
// check for estimated liquidity minted // check for estimated liquidity minted
const liquidityMinted: TokenAmount = const liquidityMinted: TokenAmount =
!!pair && !!pair &&
!!totalSupply &&
!!parsedAmounts[Field.INPUT] && !!parsedAmounts[Field.INPUT] &&
!!parsedAmounts[Field.OUTPUT] && !!parsedAmounts[Field.OUTPUT] &&
!JSBI.equal(parsedAmounts[Field.INPUT].raw, JSBI.BigInt(0)) && !JSBI.equal(parsedAmounts[Field.INPUT].raw, JSBI.BigInt(0)) &&
!JSBI.equal(parsedAmounts[Field.OUTPUT].raw, JSBI.BigInt(0)) && !JSBI.equal(parsedAmounts[Field.OUTPUT].raw, JSBI.BigInt(0))
totalSupply && ? pair.getLiquidityMinted(totalSupply, parsedAmounts[Field.INPUT], parsedAmounts[Field.OUTPUT])
totalSupply.token.equals(pair.liquidityToken) // if stale value for pair
? pair.getLiquidityMinted(
totalSupply ? totalSupply : new TokenAmount(pair?.liquidityToken, JSBI.BigInt(0)),
parsedAmounts[Field.INPUT],
parsedAmounts[Field.OUTPUT]
)
: undefined : undefined
const poolTokenPercentage: Percent = const poolTokenPercentage: Percent =

@ -129,25 +129,30 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
const pairContract: Contract = usePairContract(pair?.liquidityToken.address) const pairContract: Contract = usePairContract(pair?.liquidityToken.address)
// pool token data // pool token data
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
const userLiquidity = useTokenBalance(account, pair?.liquidityToken) const userLiquidity = useTokenBalance(account, pair?.liquidityToken)
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
// input state // input state
const [state, dispatch] = useReducer(reducer, initializeRemoveState(userLiquidity?.toExact(), token0, token1)) const [state, dispatch] = useReducer(reducer, initializeRemoveState(userLiquidity?.toExact(), token0, token1))
const { independentField, typedValue } = state const { independentField, typedValue } = state
const TokensDeposited: { [field: number]: TokenAmount } = { const tokensDeposited: { [field: number]: TokenAmount } = {
[Field.TOKEN0]: [Field.TOKEN0]:
pair && pair &&
totalPoolTokens && totalPoolTokens &&
userLiquidity && userLiquidity &&
pair.getLiquidityValue(tokens[Field.TOKEN0], totalPoolTokens, userLiquidity, false), // this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userLiquidity.raw)
? pair.getLiquidityValue(tokens[Field.TOKEN0], totalPoolTokens, userLiquidity, false)
: undefined,
[Field.TOKEN1]: [Field.TOKEN1]:
pair && pair &&
totalPoolTokens && totalPoolTokens &&
userLiquidity && userLiquidity &&
pair.getLiquidityValue(tokens[Field.TOKEN1], totalPoolTokens, userLiquidity, false) // this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userLiquidity.raw)
? pair.getLiquidityValue(tokens[Field.TOKEN1], totalPoolTokens, userLiquidity, false)
: undefined
} }
const route: Route = pair const route: Route = pair
@ -168,12 +173,12 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
if (typedValueParsed !== '0') { if (typedValueParsed !== '0') {
const tokenAmount = new TokenAmount(tokens[Field.TOKEN0], typedValueParsed) const tokenAmount = new TokenAmount(tokens[Field.TOKEN0], typedValueParsed)
if ( if (
TokensDeposited[Field.TOKEN0] && tokensDeposited[Field.TOKEN0] &&
JSBI.lessThanOrEqual(tokenAmount.raw, TokensDeposited[Field.TOKEN0].raw) JSBI.lessThanOrEqual(tokenAmount.raw, tokensDeposited[Field.TOKEN0].raw)
) { ) {
poolTokenAmount = JSBI.divide( poolTokenAmount = JSBI.divide(
JSBI.multiply(tokenAmount.raw, userLiquidity.raw), JSBI.multiply(tokenAmount.raw, userLiquidity.raw),
TokensDeposited[Field.TOKEN0].raw tokensDeposited[Field.TOKEN0].raw
) )
} }
} }
@ -183,12 +188,12 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
if (typedValueParsed !== '0') { if (typedValueParsed !== '0') {
const tokenAmount = new TokenAmount(tokens[Field.TOKEN1], typedValueParsed) const tokenAmount = new TokenAmount(tokens[Field.TOKEN1], typedValueParsed)
if ( if (
TokensDeposited[Field.TOKEN1] && tokensDeposited[Field.TOKEN1] &&
JSBI.lessThanOrEqual(tokenAmount.raw, TokensDeposited[Field.TOKEN1].raw) JSBI.lessThanOrEqual(tokenAmount.raw, tokensDeposited[Field.TOKEN1].raw)
) { ) {
poolTokenAmount = JSBI.divide( poolTokenAmount = JSBI.divide(
JSBI.multiply(tokenAmount.raw, userLiquidity.raw), JSBI.multiply(tokenAmount.raw, userLiquidity.raw),
TokensDeposited[Field.TOKEN1].raw tokensDeposited[Field.TOKEN1].raw
) )
} }
} }
@ -210,25 +215,31 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
// set parsed amounts based on live amount of liquidity // set parsed amounts based on live amount of liquidity
parsedAmounts[Field.LIQUIDITY] = parsedAmounts[Field.LIQUIDITY] =
pair && poolTokenAmount && userLiquidity && new TokenAmount(pair.liquidityToken, poolTokenAmount) !!pair && !!poolTokenAmount ? new TokenAmount(pair.liquidityToken, poolTokenAmount) : undefined
parsedAmounts[Field.TOKEN0] = parsedAmounts[Field.TOKEN0] =
totalPoolTokens && !!pair &&
pair && !!totalPoolTokens &&
parsedAmounts[Field.LIQUIDITY] && !!parsedAmounts[Field.LIQUIDITY] &&
pair.getLiquidityValue(tokens[Field.TOKEN0], totalPoolTokens, parsedAmounts[Field.LIQUIDITY], false) // this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userLiquidity.raw)
? pair.getLiquidityValue(tokens[Field.TOKEN0], totalPoolTokens, parsedAmounts[Field.LIQUIDITY], false)
: undefined
parsedAmounts[Field.TOKEN1] = parsedAmounts[Field.TOKEN1] =
totalPoolTokens && !!pair &&
pair && !!totalPoolTokens &&
parsedAmounts[Field.LIQUIDITY] && !!parsedAmounts[Field.LIQUIDITY] &&
pair.getLiquidityValue(tokens[Field.TOKEN1], totalPoolTokens, parsedAmounts[Field.LIQUIDITY], false) // this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userLiquidity.raw)
? pair.getLiquidityValue(tokens[Field.TOKEN1], totalPoolTokens, parsedAmounts[Field.LIQUIDITY], false)
: undefined
// derived percent for advanced mode // derived percent for advanced mode
const derivedPercent = const derivedPercent =
parsedAmounts[Field.LIQUIDITY] && !!parsedAmounts[Field.LIQUIDITY] && !!userLiquidity
userLiquidity && ? new Percent(parsedAmounts[Field.LIQUIDITY].raw, userLiquidity.raw)
new Percent(parsedAmounts[Field.LIQUIDITY]?.raw, userLiquidity.raw) : undefined
const [override, setSliderOverride] = useState(false) // override slider internal value const [override, setSliderOverride] = useState(false) // override slider internal value
const handlePresetPercentage = newPercent => { const handlePresetPercentage = newPercent => {