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'))
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', () => {
beforeEach(() => cy.visit('/swap'))
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', () => {
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', () => {
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', () => {
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', () => {

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

@ -1,7 +1,7 @@
import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint256 } from '@ethersproject/constants'
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 React, { useCallback, useContext, useEffect, useState } from 'react'
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_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 {
sendingInput: boolean
params: QueryParams
@ -130,8 +133,8 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
// get user- and token-specific lookup data
const userBalances = {
[Field.INPUT]: allBalances?.[tokens[Field.INPUT]?.address]?.raw,
[Field.OUTPUT]: allBalances?.[tokens[Field.OUTPUT]?.address]?.raw
[Field.INPUT]: allBalances?.[account]?.[tokens[Field.INPUT]?.address],
[Field.OUTPUT]: allBalances?.[account]?.[tokens[Field.OUTPUT]?.address]
}
// parse the amount that the user typed
@ -251,19 +254,6 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
[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
useEffect(() => {
if (sending && !sendingWithSwap) {
@ -271,39 +261,19 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
}
}, [onTokenSelection, sending, sendingWithSwap])
const MIN_ETHER: TokenAmount = chainId && new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01')))
let maxAmountInput: TokenAmount
try {
maxAmountInput =
!!userBalances[Field.INPUT] &&
!!tokens[Field.INPUT] &&
WETH[chainId] &&
JSBI.greaterThan(
userBalances[Field.INPUT].raw,
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 maxAmountInput: TokenAmount =
!!userBalances[Field.INPUT] &&
!!tokens[Field.INPUT] &&
!!WETH[chainId] &&
userBalances[Field.INPUT].greaterThan(
new TokenAmount(tokens[Field.INPUT], tokens[Field.INPUT].equals(WETH[chainId]) ? MIN_ETH : '0')
)
? tokens[Field.INPUT].equals(WETH[chainId])
? userBalances[Field.INPUT].subtract(new TokenAmount(WETH[chainId], MIN_ETH))
: userBalances[Field.INPUT]
: undefined
const atMaxAmountInput: boolean =
!!maxAmountInput && !!parsedAmounts[Field.INPUT]
? 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
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
function getSwapType(): SwapType {
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(() => {
// general fallback for tokens who restrict approval amounts
useUserBalance = true
return tokenContract.estimateGas.approve(ROUTER_ADDRESS, userBalances[field])
return tokenContract.estimateGas.approve(ROUTER_ADDRESS, userBalances[field].raw.toString())
})
tokenContract
.approve(ROUTER_ADDRESS, useUserBalance ? userBalances[field] : MaxUint256, {
.approve(ROUTER_ADDRESS, useUserBalance ? userBalances[field].raw.toString() : MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas)
})
.then(response => {
@ -585,7 +555,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
if (
userBalances[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')
setIsValid(false)
@ -905,9 +875,8 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
function _onTokenSelect(address: string) {
const balance = allBalances?.[account]?.[address]
// 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) {
onTokenSelection(Field.INPUT, null)
onTokenSelection(Field.OUTPUT, address)
@ -1047,11 +1016,10 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountOutput && onMaxOutput(maxAmountOutput.toExact())
}}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onMax={() => {}}
atMax={true}
label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'}
atMax={atMaxAmountOutput}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
advanced={advanced}
@ -1286,11 +1254,11 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Liquidity Provider Fee
</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>
<TYPE.black fontSize={14} color={theme.text1}>
{realizedLPFeeAmount
? realizedLPFeeAmount?.toSignificant(6) + ' ' + tokens[Field.INPUT]?.symbol
? realizedLPFeeAmount?.toSignificant(4) + ' ' + tokens[Field.INPUT]?.symbol
: '-'}
</TYPE.black>
</RowBetween>

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

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

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

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

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

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

@ -1,6 +1,7 @@
import { Contract } from '@ethersproject/contracts'
import { Token, TokenAmount, Pair, Trade, ChainId, WETH, Route, TradeType, Percent } from '@uniswap/sdk'
import useSWR from 'swr'
import { useWeb3React } from '@web3-react/core'
import IUniswapV1Factory from '../constants/abis/v1_factory.json'
import { V1_FACTORY_ADDRESS } from '../constants'
@ -8,31 +9,35 @@ import { useContract } from '../hooks'
import { SWRKeys } from '.'
import { useETHBalances, useTokenBalances } from '../state/wallet/hooks'
function getV1PairAddress(contract: Contract): (_: SWRKeys, tokenAddress: string) => Promise<string> {
return async (_: SWRKeys, tokenAddress: string): Promise<string> => contract.getExchange(tokenAddress)
function getV1PairAddress(contract: Contract): (tokenAddress: string) => Promise<string> {
return async (tokenAddress: string): Promise<string> => contract.getExchange(tokenAddress)
}
function useV1PairAddress(tokenAddress: string) {
const contract = useContract(V1_FACTORY_ADDRESS, IUniswapV1Factory, false)
const shouldFetch = typeof tokenAddress === 'string' && !!contract
const { chainId } = useWeb3React()
const { data } = useSWR(shouldFetch ? [SWRKeys.V1PairAddress, tokenAddress] : null, getV1PairAddress(contract), {
refreshInterval: 0 // don't need to update these
const contract = useContract(V1_FACTORY_ADDRESS, IUniswapV1Factory, false)
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
}
function useMockV1Pair(token?: Token) {
const mainnet = token?.chainId === ChainId.MAINNET
const isWETH = token?.equals(WETH[ChainId.MAINNET])
const isWETH = token?.equals(WETH[token?.chainId])
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 ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress]
return tokenBalance && ETHBalance
? new Pair(tokenBalance, new TokenAmount(WETH[ChainId.MAINNET], ETHBalance.toString()))
? new Pair(tokenBalance, new TokenAmount(WETH[token?.chainId], ETHBalance.toString()))
: undefined
}
@ -63,9 +68,20 @@ export function useV1TradeLinkIfBetter(trade: Trade, minimumDelta: Percent = new
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=${
inputIsWETH ? 'ETH' : trade.route.input.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 {
Allowances,
Reserves,
TotalSupply,
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) => {
tokenMap[token.address] = token
return tokenMap
}, ALL_TOKENS?.[chainId] ?? {})
}, ALL_TOKENS[chainId] ?? {})
)
}, [userAddedTokens, chainId])
}

@ -275,17 +275,12 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
// check for estimated liquidity minted
const liquidityMinted: TokenAmount =
!!pair &&
!!totalSupply &&
!!parsedAmounts[Field.INPUT] &&
!!parsedAmounts[Field.OUTPUT] &&
!JSBI.equal(parsedAmounts[Field.INPUT].raw, JSBI.BigInt(0)) &&
!JSBI.equal(parsedAmounts[Field.OUTPUT].raw, JSBI.BigInt(0)) &&
totalSupply &&
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]
)
!JSBI.equal(parsedAmounts[Field.OUTPUT].raw, JSBI.BigInt(0))
? pair.getLiquidityMinted(totalSupply, parsedAmounts[Field.INPUT], parsedAmounts[Field.OUTPUT])
: undefined
const poolTokenPercentage: Percent =

@ -129,25 +129,30 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
const pairContract: Contract = usePairContract(pair?.liquidityToken.address)
// pool token data
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
const userLiquidity = useTokenBalance(account, pair?.liquidityToken)
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
// input state
const [state, dispatch] = useReducer(reducer, initializeRemoveState(userLiquidity?.toExact(), token0, token1))
const { independentField, typedValue } = state
const TokensDeposited: { [field: number]: TokenAmount } = {
const tokensDeposited: { [field: number]: TokenAmount } = {
[Field.TOKEN0]:
pair &&
totalPoolTokens &&
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]:
pair &&
totalPoolTokens &&
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
@ -168,12 +173,12 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
if (typedValueParsed !== '0') {
const tokenAmount = new TokenAmount(tokens[Field.TOKEN0], typedValueParsed)
if (
TokensDeposited[Field.TOKEN0] &&
JSBI.lessThanOrEqual(tokenAmount.raw, TokensDeposited[Field.TOKEN0].raw)
tokensDeposited[Field.TOKEN0] &&
JSBI.lessThanOrEqual(tokenAmount.raw, tokensDeposited[Field.TOKEN0].raw)
) {
poolTokenAmount = JSBI.divide(
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') {
const tokenAmount = new TokenAmount(tokens[Field.TOKEN1], typedValueParsed)
if (
TokensDeposited[Field.TOKEN1] &&
JSBI.lessThanOrEqual(tokenAmount.raw, TokensDeposited[Field.TOKEN1].raw)
tokensDeposited[Field.TOKEN1] &&
JSBI.lessThanOrEqual(tokenAmount.raw, tokensDeposited[Field.TOKEN1].raw)
) {
poolTokenAmount = JSBI.divide(
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
parsedAmounts[Field.LIQUIDITY] =
pair && poolTokenAmount && userLiquidity && new TokenAmount(pair.liquidityToken, poolTokenAmount)
!!pair && !!poolTokenAmount ? new TokenAmount(pair.liquidityToken, poolTokenAmount) : undefined
parsedAmounts[Field.TOKEN0] =
totalPoolTokens &&
pair &&
parsedAmounts[Field.LIQUIDITY] &&
pair.getLiquidityValue(tokens[Field.TOKEN0], totalPoolTokens, parsedAmounts[Field.LIQUIDITY], false)
!!pair &&
!!totalPoolTokens &&
!!parsedAmounts[Field.LIQUIDITY] &&
// 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] =
totalPoolTokens &&
pair &&
parsedAmounts[Field.LIQUIDITY] &&
pair.getLiquidityValue(tokens[Field.TOKEN1], totalPoolTokens, parsedAmounts[Field.LIQUIDITY], false)
!!pair &&
!!totalPoolTokens &&
!!parsedAmounts[Field.LIQUIDITY] &&
// 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
const derivedPercent =
parsedAmounts[Field.LIQUIDITY] &&
userLiquidity &&
new Percent(parsedAmounts[Field.LIQUIDITY]?.raw, userLiquidity.raw)
!!parsedAmounts[Field.LIQUIDITY] && !!userLiquidity
? new Percent(parsedAmounts[Field.LIQUIDITY].raw, userLiquidity.raw)
: undefined
const [override, setSliderOverride] = useState(false) // override slider internal value
const handlePresetPercentage = newPercent => {