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:
parent
b1ffab1890
commit
7bffea0692
@ -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 => {
|
||||
|
Loading…
Reference in New Issue
Block a user