Commit d40d81ca authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

noah changes (#739)

* remove duplicated prettier from dependencies

* use swr for allowances

* slight improvements to error cascading

* fetch totalsupply with SWR

fetch pairs with SWR (except persistent pairs, will be refactored later)

* fix lint error

* stop using pair context, instead store per-user pairs in local storage

* add more pairs to the default list

remove unnecessary isWETH

fix lint error

* add more bases to trade consideration

* fix lint warnings

* get SWR data on same schedule

* don't pass value if it's 0

fix WETH equality checks

* clean up fixed/significant formatting

* fix slippage calculation bug in swap

add slippage to remove

* fix token logos

* show fewer sig figs

* lint error

* remove unused signer

* add calculateSlippageAmount safety check and tests

* fix useAllBalances type

stop spuriously sorting token amounts of different tokens

use slightly better comparisons for slippage calculations

* improve priceSlippage comparison logic

* useReserves -> usePair

* use checksum for DAI logo address
parent dba6abad
......@@ -34,7 +34,6 @@
"i18next-xhr-backend": "^2.0.1",
"jazzicon": "^1.5.0",
"polished": "^3.3.2",
"prettier": "^1.17.0",
"qrcode.react": "^0.9.3",
"react": "^16.13.1",
"react-device-detect": "^1.6.2",
......@@ -48,6 +47,7 @@
"react-use-gesture": "^6.0.14",
"rebass": "^4.0.7",
"styled-components": "^4.2.0",
"swr": "0.1.18",
"use-media": "^1.4.0"
},
"devDependencies": {
......
......@@ -71,7 +71,7 @@ export default function BalanceCard({ token0, balance0, import0, token1, balance
<TYPE.body fontWeight={500}>
{token0?.name} ({token0?.symbol})
</TYPE.body>
<TokenLogo size={'20px'} address={token0?.address || ''} />
<TokenLogo size={'20px'} address={token0?.address} />
</RowBetween>
{import0 && <TYPE.yellow style={{ paddingLeft: '0' }}>Token imported by user</TYPE.yellow>}
......@@ -113,7 +113,7 @@ export default function BalanceCard({ token0, balance0, import0, token1, balance
<TYPE.body fontWeight={500}>
{token1?.name} ({token1?.symbol})
</TYPE.body>
<TokenLogo size={'20px'} address={token1?.address || ''} />
<TokenLogo size={'20px'} address={token1?.address} />
</RowBetween>
{import1 && <TYPE.yellow style={{ paddingLeft: '0' }}>Token imported by user</TYPE.yellow>}
......
......@@ -12,9 +12,9 @@ import { TYPE, Link } from '../../theme'
import { AutoColumn, ColumnCenter } from '../Column'
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../Button'
import { usePair } from '../../contexts/Pairs'
import { useToken } from '../../contexts/Tokens'
import { useWeb3React } from '../../hooks'
import { usePair } from '../../data/Reserves'
const Fields = {
TOKEN0: 0,
......
import React, { useState, useCallback, useEffect, useContext } from 'react'
import { ThemeContext } from 'styled-components'
import { parseEther, parseUnits } from '@ethersproject/units'
import { Fraction, JSBI, Percent, TokenAmount, TradeType, WETH } from '@uniswap/sdk'
import { JSBI, Percent, TokenAmount, TradeType, WETH, Fraction } from '@uniswap/sdk'
import { ArrowDown, ChevronDown, ChevronUp, Repeat } from 'react-feather'
import { withRouter, RouteComponentProps } from 'react-router-dom'
import { BigNumber } from '@ethersproject/bignumber'
import { Zero, MaxUint256 } from '@ethersproject/constants'
import { MaxUint256 } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts'
import { Field, SwapAction, useSwapStateReducer } from './swap-store'
import { Text } from 'rebass'
......@@ -13,11 +13,11 @@ import Card, { BlueCard, GreyCard, YellowCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { AutoRow, RowBetween, RowFixed } from '../Row'
import { ROUTER_ADDRESS } from '../../constants'
import { useAddressAllowance } from '../../contexts/Allowances'
import { useTokenAllowance } from '../../data/Allowances'
import { useUserAdvanced } from '../../contexts/Application'
import { useAddressBalance, useAllBalances } from '../../contexts/Balances'
import { useLocalStorageTokens } from '../../contexts/LocalStorage'
import { usePair } from '../../contexts/Pairs'
import { usePair } from '../../data/Reserves'
import { useAllTokens, useToken } from '../../contexts/Tokens'
import { usePendingApproval, useTransactionAdder } from '../../contexts/Transactions'
import { useTokenContract, useWeb3React } from '../../hooks'
......@@ -29,8 +29,8 @@ import {
getEtherscanLink,
getProviderOrSigner,
getRouterContract,
isWETH,
QueryParams
QueryParams,
calculateSlippageAmount
} from '../../utils'
import Copy from '../AccountDetails/Copy'
import AddressInputPanel from '../AddressInputPanel'
......@@ -55,12 +55,6 @@ import {
Wrapper
} from './styleds'
// import BalanceCard from '../BalanceCard'
function hex(value: JSBI) {
return BigNumber.from(value.toString())
}
enum SwapType {
EXACT_TOKENS_FOR_TOKENS,
EXACT_TOKENS_FOR_ETH,
......@@ -141,9 +135,6 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
const tokenContractInput: Contract = useTokenContract(tokens[Field.INPUT]?.address)
const tokenContractOutput: Contract = useTokenContract(tokens[Field.OUTPUT]?.address)
// check on pending approvals for token amounts
const pendingApprovalInput = usePendingApproval(tokens[Field.INPUT]?.address)
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
......@@ -155,11 +146,8 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
// input approval
const inputApproval: TokenAmount = useAddressAllowance(account, tokens[Field.INPUT], ROUTER_ADDRESS)
// all balances for detecting a swap with send
const allBalances: TokenAmount[] = useAllBalances()
const allBalances = useAllBalances()
// get user- and token-specific lookup data
const userBalances = {
......@@ -167,6 +155,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
[Field.OUTPUT]: useAddressBalance(account, tokens[Field.OUTPUT])
}
// parse the amount that the user typed
const parsedAmounts: { [field: number]: TokenAmount } = {}
if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
try {
......@@ -174,11 +163,13 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
if (typedValueParsed !== '0')
parsedAmounts[independentField] = new TokenAmount(tokens[independentField], typedValueParsed)
} catch (error) {
// should only fail if the user specifies too many decimal places of precision (or maybe exceed max uint?)
console.error(error)
}
}
const pair = usePair(tokens[Field.INPUT], tokens[Field.OUTPUT])
const bestTradeExactIn = useTradeExactIn(
tradeType === TradeType.EXACT_INPUT ? parsedAmounts[independentField] : null,
tokens[Field.OUTPUT]
......@@ -191,10 +182,10 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
const trade = tradeType === TradeType.EXACT_INPUT ? bestTradeExactIn : bestTradeExactOut
const route = trade?.route
const userHasSpecifiedInputOutput =
!!parsedAmounts[independentField] &&
parsedAmounts[independentField].greaterThan(JSBI.BigInt(0)) &&
!!tokens[Field.INPUT] &&
!!tokens[Field.OUTPUT]
!!tokens[Field.OUTPUT] &&
!!parsedAmounts[independentField] &&
parsedAmounts[independentField].greaterThan(JSBI.BigInt(0))
const noRoute = !route
const slippageFromTrade: Percent = trade && trade.slippage
......@@ -202,6 +193,15 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
if (trade)
parsedAmounts[dependentField] = tradeType === TradeType.EXACT_INPUT ? trade.outputAmount : trade.inputAmount
// check whether the user has approved the router on the input token
const inputApproval: TokenAmount = useTokenAllowance(tokens[Field.INPUT], account, ROUTER_ADDRESS)
const userHasApprovedRouter =
tokens[Field.INPUT]?.equals(WETH[chainId]) ||
(!!inputApproval &&
!!parsedAmounts[Field.INPUT] &&
JSBI.greaterThanOrEqual(inputApproval.raw, parsedAmounts[Field.INPUT].raw))
const pendingApprovalInput = usePendingApproval(tokens[Field.INPUT]?.address)
const feeAsPercent = new Percent(JSBI.BigInt(3), JSBI.BigInt(1000))
const feeTimesInputRaw =
parsedAmounts[Field.INPUT] && feeAsPercent.multiply(JSBI.BigInt(parsedAmounts[Field.INPUT].raw))
......@@ -209,17 +209,14 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(8) : ''
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
}
const priceSlippage =
slippageFromTrade &&
new Percent(
JSBI.subtract(
JSBI.multiply(slippageFromTrade.numerator, JSBI.BigInt('1000')),
JSBI.multiply(JSBI.BigInt('3'), slippageFromTrade.denominator)
),
JSBI.multiply(slippageFromTrade.denominator, JSBI.BigInt('1000'))
slippageFromTrade.subtract(new Fraction('30', '10000')).numerator,
slippageFromTrade.subtract(new Fraction('30', '10000')).denominator
)
const onTokenSelection = useCallback(
......@@ -288,8 +285,11 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
!!userBalances[Field.INPUT] &&
!!tokens[Field.INPUT] &&
WETH[chainId] &&
JSBI.greaterThan(userBalances[Field.INPUT].raw, isWETH(tokens[Field.INPUT]) ? MIN_ETHER.raw : JSBI.BigInt(0))
? isWETH(tokens[Field.INPUT])
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
......@@ -312,17 +312,17 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
function getSwapType(): SwapType {
if (tradeType === TradeType.EXACT_INPUT) {
if (tokens[Field.INPUT] === WETH[chainId]) {
if (tokens[Field.INPUT].equals(WETH[chainId])) {
return SwapType.EXACT_ETH_FOR_TOKENS
} else if (tokens[Field.OUTPUT] === WETH[chainId]) {
} else if (tokens[Field.OUTPUT].equals(WETH[chainId])) {
return SwapType.EXACT_TOKENS_FOR_ETH
} else {
return SwapType.EXACT_TOKENS_FOR_TOKENS
}
} else if (tradeType === TradeType.EXACT_OUTPUT) {
if (tokens[Field.INPUT] === WETH[chainId]) {
if (tokens[Field.INPUT].equals(WETH[chainId])) {
return SwapType.ETH_FOR_EXACT_TOKENS
} else if (tokens[Field.OUTPUT] === WETH[chainId]) {
} else if (tokens[Field.OUTPUT].equals(WETH[chainId])) {
return SwapType.TOKENS_FOR_EXACT_ETH
} else {
return SwapType.TOKENS_FOR_EXACT_TOKENS
......@@ -330,30 +330,21 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
}
}
function calculateSlippageAmount(value: TokenAmount): JSBI[] {
if (value && value.raw) {
const offset = JSBI.divide(JSBI.multiply(JSBI.BigInt(allowedSlippage), value.raw), JSBI.BigInt(10000))
return [JSBI.subtract(value.raw, offset), JSBI.add(value.raw, offset)]
}
return null
}
const slippageAdjustedAmounts: { [field in Field]: TokenAmount } = {
[Field.INPUT]:
Field.INPUT === independentField
? parsedAmounts[Field.INPUT]
: calculateSlippageAmount(parsedAmounts[Field.INPUT])?.[0] &&
new TokenAmount(tokens[Field.INPUT], calculateSlippageAmount(parsedAmounts[Field.INPUT])?.[1]),
[Field.OUTPUT]:
Field.OUTPUT === independentField
? parsedAmounts[Field.OUTPUT]
: calculateSlippageAmount(parsedAmounts[Field.OUTPUT])?.[0] &&
new TokenAmount(tokens[Field.INPUT], calculateSlippageAmount(parsedAmounts[Field.OUTPUT])?.[0])
const slippageAdjustedAmounts: { [field: number]: TokenAmount } = {
[independentField]: parsedAmounts[independentField],
[dependentField]: parsedAmounts[dependentField]
? tradeType === TradeType.EXACT_INPUT
? new TokenAmount(
tokens[dependentField],
calculateSlippageAmount(parsedAmounts[dependentField], allowedSlippage)[0]
)
: new TokenAmount(
tokens[dependentField],
calculateSlippageAmount(parsedAmounts[dependentField], allowedSlippage)[1]
)
: undefined
}
const showInputApprove: boolean =
parsedAmounts[Field.INPUT] && inputApproval && JSBI.greaterThan(parsedAmounts[Field.INPUT].raw, inputApproval.raw)
// reset modal state when closed
function resetModal() {
// clear input if txn submitted
......@@ -371,10 +362,10 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
const signer = await getProviderOrSigner(library, account)
// get token contract if needed
let estimate: Function, method: Function, args, value
if (tokens[Field.INPUT] === WETH[chainId]) {
let estimate: Function, method: Function, args
if (tokens[Field.INPUT].equals(WETH[chainId])) {
;(signer as any)
.sendTransaction({ to: recipient.toString(), value: hex(parsedAmounts[Field.INPUT].raw) })
.sendTransaction({ to: recipient.toString(), value: BigNumber.from(parsedAmounts[Field.INPUT].raw.toString()) })
.then(response => {
setTxHash(response.hash)
addTransaction(
......@@ -396,11 +387,9 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
estimate = tokenContractInput.estimateGas.transfer
method = tokenContractInput.transfer
args = [recipient, parsedAmounts[Field.INPUT].raw.toString()]
value = Zero
await estimate(...args, { value })
await estimate(...args)
.then(estimatedGasLimit =>
method(...args, {
value,
gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => {
setTxHash(response.hash)
......@@ -446,7 +435,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
sending ? recipient : account,
deadlineFromNow
]
value = Zero
value = null
break
case SwapType.TOKENS_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapTokensForExactTokens
......@@ -458,7 +447,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
sending ? recipient : account,
deadlineFromNow
]
value = Zero
value = null
break
case SwapType.EXACT_ETH_FOR_TOKENS:
estimate = routerContract.estimateGas.swapExactETHForTokens
......@@ -469,7 +458,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
sending ? recipient : account,
deadlineFromNow
]
value = hex(slippageAdjustedAmounts[Field.INPUT].raw)
value = BigNumber.from(slippageAdjustedAmounts[Field.INPUT].raw.toString())
break
case SwapType.TOKENS_FOR_EXACT_ETH:
estimate = routerContract.estimateGas.swapTokensForExactETH
......@@ -481,7 +470,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
sending ? recipient : account,
deadlineFromNow
]
value = Zero
value = null
break
case SwapType.EXACT_TOKENS_FOR_ETH:
estimate = routerContract.estimateGas.swapExactTokensForETH
......@@ -493,7 +482,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
sending ? recipient : account,
deadlineFromNow
]
value = Zero
value = null
break
case SwapType.ETH_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapETHForExactTokens
......@@ -504,14 +493,14 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
sending ? recipient : account,
deadlineFromNow
]
value = hex(slippageAdjustedAmounts[Field.INPUT].raw)
value = BigNumber.from(slippageAdjustedAmounts[Field.INPUT].raw.toString())
break
}
await estimate(...args, { value })
await estimate(...args, value ? { value } : {})
.then(estimatedGasLimit =>
method(...args, {
value,
...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => {
setTxHash(response.hash)
......@@ -579,6 +568,11 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
setIsValid(false)
}
if (!account) {
setGeneralError('Connect Wallet')
setIsValid(false)
}
if (!parsedAmounts[Field.INPUT]) {
setInputError('Enter an amount')
setIsValid(false)
......@@ -643,18 +637,20 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
tokens,
route,
trade,
userBalances
userBalances,
account
])
// warnings on slippage
const warningLow: boolean =
slippageFromTrade &&
parseFloat(slippageFromTrade.toFixed(4)) < ALLOWED_SLIPPAGE_MEDIUM / 100 &&
parseFloat(slippageFromTrade.toFixed(4)) > 0
const warningLow: boolean = slippageFromTrade?.lessThan(new Percent(ALLOWED_SLIPPAGE_MEDIUM.toString(), '10000'))
// TODO greaterThanOrEqualTo in SDK
const warningMedium: boolean =
slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_MEDIUM / 100
slippageFromTrade?.equalTo(new Percent(ALLOWED_SLIPPAGE_MEDIUM.toString(), '10000')) ||
slippageFromTrade?.greaterThan(new Percent(ALLOWED_SLIPPAGE_MEDIUM.toString(), '10000'))
// TODO greaterThanOrEqualTo in SDK
const warningHigh: boolean =
slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_HIGH / 100
slippageFromTrade?.equalTo(new Percent(ALLOWED_SLIPPAGE_HIGH.toString(), '10000')) ||
slippageFromTrade?.greaterThan(new Percent(ALLOWED_SLIPPAGE_HIGH.toString(), '10000'))
function modalHeader() {
if (sending && !sendingWithSwap) {
......@@ -751,8 +747,8 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
{`Output is estimated. You will receive at least `}
<b>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}{' '}
</b>{' '}
{` or the transaction will revert.`}
</b>
{' or the transaction will revert.'}
</TYPE.italic>
) : (
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
......@@ -760,7 +756,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
<b>
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {tokens[Field.INPUT]?.symbol}
</b>
{` or the transaction will revert.`}
{' or the transaction will revert.'}
</TYPE.italic>
)}
</AutoColumn>
......@@ -824,16 +820,8 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
<RowFixed>
<TYPE.black fontSize={14}>
{independentField === Field.INPUT
? slippageAdjustedAmounts[Field.OUTPUT]
? slippageAdjustedAmounts[Field.OUTPUT]?.toFixed(5) === '0.00000'
? '<0.00001'
: slippageAdjustedAmounts[Field.OUTPUT]?.toFixed(5)
: '-'
: slippageAdjustedAmounts[Field.INPUT]
? slippageAdjustedAmounts[Field.INPUT]?.toFixed(5) === '0.00000'
? '<0.00001'
: slippageAdjustedAmounts[Field.INPUT]?.toFixed(5)
: '-'}
? slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4) ?? '-'
: slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4) ?? '-'}
</TYPE.black>
{parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && (
<TYPE.black fontSize={14} marginLeft={'4px'}>
......@@ -858,11 +846,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
warningMedium={warningMedium}
warningHigh={warningHigh}
>
{priceSlippage
? priceSlippage.toFixed(4) === '0.0000'
? '<0.0001%'
: priceSlippage.toFixed(4) + '%'
: '-'}
{priceSlippage?.lessThan(new Percent('1', '10000')) ? '<0.01%' : `${priceSlippage?.toFixed(2)}%` ?? '-'}
</ErrorText>
</RowBetween>
<RowBetween>
......@@ -920,11 +904,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
warningMedium={warningMedium}
warningHigh={warningHigh}
>
{priceSlippage
? priceSlippage.toFixed(4) === '0.0000'
? '<0.0001%'
: priceSlippage.toFixed(4) + '%'
: '-'}
{priceSlippage?.lessThan(new Percent('1', '10000')) ? '<0.01%' : `${priceSlippage?.toFixed(2)}%` ?? '-'}
</ErrorText>
<Text fontWeight={500} fontSize={16} color={theme.text3} pt={1}>
Price Impact
......@@ -1130,7 +1110,6 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
<PriceBar />
) : (
<AutoColumn gap="4px">
{' '}
<RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}>
Price
......@@ -1167,11 +1146,9 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
</TYPE.main>
<RowFixed>
<ErrorText fontWeight={500} fontSize={14} warningMedium={warningMedium} warningHigh={warningHigh}>
{priceSlippage
? priceSlippage.toFixed(4) === '0.0000'
? '<0.0001%'
: priceSlippage.toFixed(4) + '%'
: '-'}{' '}
{priceSlippage?.lessThan(new Percent('1', '10000'))
? '<0.01%'
: `${priceSlippage?.toFixed(2)}%` ?? '-'}
</ErrorText>
<QuestionHelper text="The difference between the market price and your quoted price due to trade size." />
</RowFixed>
......@@ -1192,11 +1169,10 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
history.push('/add/' + tokens[Field.INPUT]?.address + '-' + tokens[Field.OUTPUT]?.address)
}}
>
{' '}
Create one now
</Link>
</GreyCard>
) : showInputApprove && !inputError ? (
) : !userHasApprovedRouter && !inputError ? (
<ButtonLight
onClick={() => {
approveAmount(Field.INPUT)
......@@ -1218,9 +1194,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
error={!!warningHigh}
>
<Text fontSize={20} fontWeight={500}>
{!account
? 'Connect Wallet'
: generalError ||
{generalError ||
inputError ||
outputError ||
recipientError ||
......@@ -1276,20 +1250,8 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
<RowFixed>
<TYPE.black color={theme.text1} fontSize={14}>
{independentField === Field.INPUT
? slippageAdjustedAmounts[Field.OUTPUT]
? slippageAdjustedAmounts[Field.OUTPUT]?.lessThan(
new Fraction(JSBI.BigInt(1), JSBI.BigInt(10000))
)
? '<0.00001'
: slippageAdjustedAmounts[Field.OUTPUT]?.toFixed(5)
: '-'
: slippageAdjustedAmounts[Field.INPUT]
? slippageAdjustedAmounts[Field.INPUT]?.lessThan(
new Fraction(JSBI.BigInt(1), JSBI.BigInt(10000))
)
? '<0.00001'
: slippageAdjustedAmounts[Field.INPUT]?.toFixed(5)
: '-'}
? slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4) ?? '-'
: slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4) ?? '-'}
</TYPE.black>
{parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && (
<TYPE.black fontSize={14} marginLeft={'4px'} color={theme.text1}>
......@@ -1314,11 +1276,9 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
warningMedium={warningMedium}
warningHigh={warningHigh}
>
{priceSlippage
? priceSlippage.toFixed(4) === '0.0000'
? '<0.0001%'
: priceSlippage.toFixed(4) + '%'
: '-'}
{priceSlippage?.lessThan(new Percent('1', '10000'))
? '<0.01%'
: `${priceSlippage?.toFixed(2)}%` ?? '-'}
</ErrorText>
</RowBetween>
<RowBetween>
......@@ -1366,8 +1326,8 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
</RowFixed>
</RowBetween>
<Text lineHeight="145.23%;" fontSize={16} fontWeight={400} color={theme.text1}>
This trade will move the price by {slippageFromTrade.toFixed(2)}%. This pool probably doesn’t have
enough liquidity to support this trade.
This trade will move the price by {slippageFromTrade?.toFixed(2)}%. This pool probably doesn’t
have enough liquidity to support this trade.
</Text>
</AutoColumn>
</YellowCard>
......
......@@ -205,7 +205,7 @@ export default function Header() {
<AccountElement active={!!account}>
{account ? (
<Row style={{ marginRight: '-1.25rem' }}>
<Text fontWeight={500}> {userEthBalance && userEthBalance?.toFixed(4) + ' ETH'}</Text>
<Text fontWeight={500}> {userEthBalance && `${userEthBalance?.toSignificant(4)} ETH`}</Text>
</Row>
) : (
''
......
......@@ -6,7 +6,6 @@ import Row from '../Row'
import TokenLogo from '../TokenLogo'
import SearchModal from '../SearchModal'
import PositionCard from '../PositionCard'
import DoubleTokenLogo from '../DoubleLogo'
import { Link } from '../../theme'
import { Text } from 'rebass'
import { Plus } from 'react-feather'
......@@ -15,71 +14,40 @@ import { AutoColumn, ColumnCenter } from '../Column'
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../Button'
import { useToken } from '../../contexts/Tokens'
import { usePopups } from '../../contexts/Application'
import { usePrevious } from '../../hooks'
import { useWeb3React } from '@web3-react/core'
import { useAddressBalance } from '../../contexts/Balances'
import { usePair, useAllPairs } from '../../contexts/Pairs'
import { useLocalStoragePairAdder } from '../../contexts/LocalStorage'
import { usePair } from '../../data/Reserves'
function PoolFinder({ history }: RouteComponentProps) {
const Fields = {
const Fields = {
TOKEN0: 0,
TOKEN1: 1
}
}
function PoolFinder({ history }: RouteComponentProps) {
const { account } = useWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
const [, addPopup] = usePopups()
const [token0Address, setToken0Address] = useState<string>()
const [token1Address, setToken1Address] = useState<string>()
const token0: Token = useToken(token0Address)
const token1: Token = useToken(token1Address)
const pair: Pair = usePair(token0, token1)
const position: TokenAmount = useAddressBalance(account, pair?.liquidityToken)
const newPair: boolean = pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0))
const allowImport: boolean = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
const addPair = useLocalStoragePairAdder()
const allPairs = useAllPairs()
const pairCount = Object.keys(allPairs)?.length
const pairCountPrevious = usePrevious(pairCount)
const [newLiquidity, setNewLiquidity] = useState<boolean>(false) // check for unimported pair
// use previous ref to detect new pair added
useEffect(() => {
if (pairCount !== pairCountPrevious && pairCountPrevious) {
setNewLiquidity(true)
if (pair) {
addPair(pair)
}
}, [pairCount, pairCountPrevious])
}, [pair, addPair])
// reset the watcher if tokens change
useEffect(() => {
setNewLiquidity(false)
}, [token0, token1])
const position: TokenAmount = useAddressBalance(account, pair?.liquidityToken)
function endSearch() {
history.goBack() // return to previous page
newLiquidity &&
addPopup(
<AutoColumn gap={'10px'}>
<Text fontSize={20} fontWeight={500}>
Pool Imported
</Text>
<Row>
<DoubleTokenLogo a0={token0Address || ''} a1={token1Address || ''} margin={true} />
<Text fontSize={16} fontWeight={500}>
UNI {token0?.symbol} / {token1?.symbol}
</Text>
</Row>
<Link>View on Uniswap Info.</Link>
</AutoColumn>
)
}
const newPair: boolean =
!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0))
const allowImport: boolean = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
return (
<>
......@@ -140,19 +108,13 @@ function PoolFinder({ history }: RouteComponentProps) {
style={{ justifyItems: 'center', backgroundColor: '', padding: '12px 0px', borderRadius: '12px' }}
>
<Text textAlign="center" fontWeight={500} color="">
{newLiquidity ? 'Pool Found!' : 'Pool already imported.'}
Pool Imported!
</Text>
</ColumnCenter>
)}
{position ? (
!JSBI.equal(position.raw, JSBI.BigInt(0)) ? (
<PositionCard
pairAddress={pair?.liquidityToken.address}
token0={token0}
token1={token1}
minimal={true}
border="1px solid #CED0D9"
/>
<PositionCard pair={pair} minimal={true} border="1px solid #CED0D9" />
) : (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
......@@ -176,7 +138,7 @@ function PoolFinder({ history }: RouteComponentProps) {
history.push('/add/' + token0Address + '-' + token1Address)
}}
>
Create pool instead.
Create pool?
</Link>
</AutoColumn>
</LightCard>
......@@ -188,9 +150,9 @@ function PoolFinder({ history }: RouteComponentProps) {
</LightCard>
)}
<ButtonPrimary disabled={!allowImport} onClick={endSearch}>
<ButtonPrimary disabled={!allowImport} onClick={() => history.goBack()}>
<Text fontWeight={500} fontSize={20}>
{newLiquidity ? 'Import' : 'Close'}
Close
</Text>
</ButtonPrimary>
</AutoColumn>
......
......@@ -2,11 +2,11 @@ import React, { useState } from 'react'
import styled from 'styled-components'
import { darken } from 'polished'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { Percent, Pair, Token } from '@uniswap/sdk'
import { Percent, Pair } from '@uniswap/sdk'
import { useWeb3React } from '@web3-react/core'
import { useAllBalances } from '../../contexts/Balances'
import { useTotalSupply } from '../../contexts/Pairs'
import { useTotalSupply } from '../../data/TotalSupply'
import Card, { GreyCard } from '../Card'
import TokenLogo from '../TokenLogo'
......@@ -30,26 +30,22 @@ const HoverCard = styled(Card)`
`
interface PositionCardProps extends RouteComponentProps<{}> {
pairAddress: string
token0: Token
token1: Token
pair: Pair
minimal?: boolean
border?: string
}
function PositionCard({ pairAddress, token0, token1, history, border, minimal = false }: PositionCardProps) {
function PositionCard({ pair, history, border, minimal = false }: PositionCardProps) {
const { account } = useWeb3React()
const allBalances = useAllBalances()
const [showMore, setShowMore] = useState(false)
const tokenAmount0 = allBalances?.[pairAddress]?.[token0?.address]
const tokenAmount1 = allBalances?.[pairAddress]?.[token1?.address]
const token0 = pair?.token0
const token1 = pair?.token1
const pair = tokenAmount0 && tokenAmount1 && new Pair(tokenAmount0, tokenAmount1)
const [showMore, setShowMore] = useState(false)
const userPoolBalance = allBalances?.[account]?.[pairAddress]
const totalPoolTokens = useTotalSupply(token0, token1)
const userPoolBalance = allBalances?.[account]?.[pair?.liquidityToken?.address]
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
const poolTokenPercentage =
!!userPoolBalance && !!totalPoolTokens ? new Percent(userPoolBalance.raw, totalPoolTokens.raw) : undefined
......@@ -73,7 +69,7 @@ function PositionCard({ pairAddress, token0, token1, history, border, minimal =
if (minimal) {
return (
<>
{userPoolBalance && userPoolBalance.toFixed(6) > 0 && (
{userPoolBalance && (
<GreyCard border={border}>
<AutoColumn gap="12px">
<FixedHeightRow>
......@@ -92,7 +88,7 @@ function PositionCard({ pairAddress, token0, token1, history, border, minimal =
</RowFixed>
<RowFixed>
<Text fontWeight={500} fontSize={20}>
{userPoolBalance ? userPoolBalance.toFixed(6) : '-'}
{userPoolBalance ? userPoolBalance.toSignificant(5) : '-'}
</Text>
</RowFixed>
</FixedHeightRow>
......@@ -103,9 +99,9 @@ function PositionCard({ pairAddress, token0, token1, history, border, minimal =
</Text>
{token0Deposited ? (
<RowFixed>
{!minimal && <TokenLogo address={token0?.address || ''} />}
{!minimal && <TokenLogo address={token0?.address} />}
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token0Deposited?.toFixed(8)}
{token0Deposited?.toSignificant(6)}
</Text>
</RowFixed>
) : (
......@@ -118,9 +114,9 @@ function PositionCard({ pairAddress, token0, token1, history, border, minimal =
</Text>
{token1Deposited ? (
<RowFixed>
{!minimal && <TokenLogo address={token1?.address || ''} />}
{!minimal && <TokenLogo address={token1?.address} />}
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token1Deposited?.toFixed(8)}
{token1Deposited?.toSignificant(6)}
</Text>
</RowFixed>
) : (
......@@ -163,11 +159,9 @@ function PositionCard({ pairAddress, token0, token1, history, border, minimal =
{token0Deposited ? (
<RowFixed>
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token0Deposited?.toFixed(8)}
{token0Deposited?.toSignificant(6)}
</Text>
{!minimal && (
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address || ''} />
)}
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />}
</RowFixed>
) : (
'-'
......@@ -183,11 +177,9 @@ function PositionCard({ pairAddress, token0, token1, history, border, minimal =
{token1Deposited ? (
<RowFixed>
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token1Deposited?.toFixed(8)}
{token1Deposited?.toSignificant(6)}
</Text>
{!minimal && (
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address || ''} />
)}
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />}
</RowFixed>
) : (
'-'
......@@ -199,7 +191,7 @@ function PositionCard({ pairAddress, token0, token1, history, border, minimal =
Your pool tokens:
</Text>
<Text fontSize={16} fontWeight={500}>
{userPoolBalance ? userPoolBalance.toFixed(6) : '-'}
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
</Text>
</FixedHeightRow>
)}
......
......@@ -23,9 +23,8 @@ import { Spinner, TYPE } from '../../theme'
import { RowBetween, RowFixed, AutoRow } from '../Row'
import { isAddress } from '../../utils'
import { useAllPairs } from '../../contexts/Pairs'
import { useWeb3React } from '../../hooks'
import { useLocalStorageTokens } from '../../contexts/LocalStorage'
import { useLocalStorageTokens, useAllDummyPairs } from '../../contexts/LocalStorage'
import { useAllBalances } from '../../contexts/Balances'
import { useTranslation } from 'react-i18next'
import { useToken, useAllTokens, ALL_TOKENS } from '../../contexts/Tokens'
......@@ -174,7 +173,7 @@ function SearchModal({
const theme = useContext(ThemeContext)
const allTokens = useAllTokens()
const allPairs = useAllPairs()
const allPairs = useAllDummyPairs()
const allBalances = useAllBalances()
const [searchQuery, setSearchQuery] = useState('')
......@@ -226,15 +225,12 @@ function SearchModal({
const balanceA = allBalances?.[account]?.[a]
const balanceB = allBalances?.[account]?.[b]
if (balanceA && !balanceB) {
if (balanceA?.greaterThan('0') && !balanceB?.greaterThan('0')) {
return sortDirection ? -1 : 1
}
if (!balanceA && balanceB) {
if (!balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) {
return sortDirection ? 1 : -1
}
if (balanceA && balanceB) {
return sortDirection && parseFloat(balanceA.toExact()) > parseFloat(balanceB.toExact()) ? -1 : 1
}
return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
} else {
return 0
......@@ -306,20 +302,16 @@ function SearchModal({
const escapeStringRegexp = string => string
const sortedPairList = useMemo(() => {
return Object.keys(allPairs).sort((a, b): number => {
return allPairs.sort((a, b): number => {
// sort by balance
const balanceA = allBalances?.[account]?.[a]
const balanceB = allBalances?.[account]?.[b]
const balanceA = allBalances?.[account]?.[a.liquidityToken.address]
const balanceB = allBalances?.[account]?.[b.liquidityToken.address]
if (balanceA && !balanceB) {
if (balanceA?.greaterThan('0') && !balanceB?.greaterThan('0')) {
return sortDirection ? -1 : 1
}
if (!balanceA && balanceB) {
if (!balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) {
return sortDirection ? 1 : -1
}
if (balanceA && balanceB) {
const order = sortDirection && (parseFloat(balanceA.toExact()) > parseFloat(balanceB.toExact()) ? -1 : 1)
return order ? 1 : -1
} else {
return 0
}
......@@ -328,13 +320,12 @@ function SearchModal({
const filteredPairList = useMemo(() => {
const isAddress = searchQuery.slice(0, 2) === '0x'
return sortedPairList.filter(pairAddress => {
const pair = allPairs[pairAddress]
return sortedPairList.filter(pair => {
if (searchQuery === '') {
return true
}
const token0 = allTokens[pair.token0]
const token1 = allTokens[pair.token1]
const token0 = pair.token0
const token1 = pair.token1
if (!token0 || !token1) {
return false // no token fetched yet
} else {
......@@ -354,7 +345,7 @@ function SearchModal({
return regexMatches.some(m => m)
}
})
}, [allPairs, allTokens, searchQuery, sortedPairList])
}, [searchQuery, sortedPairList])
function renderPairsList() {
if (filteredPairList?.length === 0) {
......@@ -367,9 +358,10 @@ function SearchModal({
return (
filteredPairList &&
filteredPairList.map((pairAddress, i) => {
const token0 = allTokens[allPairs[pairAddress].token0]
const token1 = allTokens[allPairs[pairAddress].token1]
filteredPairList.map((pair, i) => {
const token0 = pair.token0
const token1 = pair.token1
const pairAddress = pair.liquidityToken.address
const balance = allBalances?.[account]?.[pairAddress]?.toSignificant(6)
const zeroBalance =
allBalances?.[account]?.[pairAddress]?.raw &&
......@@ -451,16 +443,17 @@ function SearchModal({
*/
return filteredTokenList
.sort((a, b) => {
if (b?.address === WETH[chainId]?.address) {
if (a.address === WETH[chainId].address) {
return -1
} else if (b.address === WETH[chainId].address) {
return 1
} else
return parseFloat(a?.balance?.toExact()) > parseFloat(b?.balance?.toExact())
? sortDirection
? -1
: 1
: sortDirection
? 1
: -1
} else if (a.balance?.greaterThan('0') && !b.balance?.greaterThan('0')) {
return sortDirection ? -1 : 1
} else if (!a.balance?.greaterThan('0') && b.balance?.greaterThan('0')) {
return sortDirection ? 1 : -1
} else {
return sortDirection ? -1 : 1
}
})
.map(({ address, symbol, balance }) => {
const urlAdded = urlAddedTokens && urlAddedTokens.hasOwnProperty(address)
......@@ -580,13 +573,7 @@ function SearchModal({
<TYPE.body style={{ marginTop: '10px' }}>
To import a custom token, paste token address in the search bar.
</TYPE.body>
<Input
type={'text'}
placeholder={'0x0000000000...'}
value={searchQuery}
ref={inputRef}
onChange={onInput}
/>
<Input type={'text'} placeholder={'0x000000...'} value={searchQuery} ref={inputRef} onChange={onInput} />
{renderTokenList()}
</PaddedColumn>
) : (
......
......@@ -7,9 +7,7 @@ import { WETH } from '@uniswap/sdk'
import { ReactComponent as EthereumLogo } from '../../assets/images/ethereum-logo.svg'
const TOKEN_ICON_API = address =>
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${isAddress(
address
)}/logo.png`
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
const BAD_IMAGES = {}
const Image = styled.img<{ size: string }>`
......@@ -40,25 +38,21 @@ export default function TokenLogo({
size = '24px',
...rest
}: {
address: string
address?: string
size?: string
style?: React.CSSProperties
}) {
const [error, setError] = useState(false)
const { chainId } = useWeb3React()
// hard code change to show ETH instead of WETH in UI
if (address === WETH[chainId].address) {
address = 'ETH'
}
// remove this just for testing
if (address === isAddress('0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735')) {
address = '0x6b175474e89094c44da98b954eedeac495271d0f'
// mock rinkeby DAI
if (chainId === 4 && address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
address = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
}
let path = ''
if (address === 'ETH') {
// hard code to show ETH instead of WETH in UI
if (address === WETH[chainId].address) {
return (
<StyledEthereumLogo
style={{ boxShadow: '0px 6px 10px rgba(0, 0, 0, 0.075)', borderRadius: '24px' }}
......@@ -66,8 +60,8 @@ export default function TokenLogo({
{...rest}
/>
)
} else if (!error && !BAD_IMAGES[address]) {
path = TOKEN_ICON_API(address?.toLowerCase())
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) {
path = TOKEN_ICON_API(address)
} else {
return (
<Emoji {...rest} size={size}>
......
import { BigintIsh, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { BigNumber } from 'ethers/utils'
import React, { createContext, useCallback, useContext, useEffect, useMemo, useReducer } from 'react'
import { useWeb3React } from '../hooks'
import { getTokenAllowance, isAddress } from '../utils'
import { useBlockNumber } from './Application'
const UPDATE = 'UPDATE'
interface AllowancesState {
[chainId: number]: {
[address: string]: {
[tokenAddress: string]: {
[spenderAddress: string]: {
value: BigintIsh
blockNumber: BigNumber
}
}
}
}
}
const AllowancesContext = createContext<[AllowancesState, any]>([{}, {}])
function useAllowancesContext() {
return useContext(AllowancesContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case UPDATE: {
const { networkId, address, tokenAddress, spenderAddress, value, blockNumber } = payload
return {
...state,
[networkId]: {
...state?.[networkId],
[address]: {
...state?.[networkId]?.[address],
[tokenAddress]: {
...state?.[networkId]?.[address]?.[tokenAddress],
[spenderAddress]: {
value,
blockNumber
}
}
}
}
}
}
default:
throw Error(`Unexpected action type in AllowancesContext reducer: '${type}'.`)
}
}
export default function Provider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, {})
const update = useCallback((networkId, address, tokenAddress, spenderAddress, value, blockNumber) => {
dispatch({ type: UPDATE, payload: { networkId, address, tokenAddress, spenderAddress, value, blockNumber } })
}, [])
return (
<AllowancesContext.Provider value={useMemo(() => [state, { update }], [state, update])}>
{children}
</AllowancesContext.Provider>
)
}
export function useAddressAllowance(address: string, token: Token, spenderAddress: string): TokenAmount {
const { library, chainId } = useWeb3React()
const globalBlockNumber = useBlockNumber()
const [state, { update }] = useAllowancesContext()
const { value, blockNumber } = state?.[chainId]?.[address]?.[token?.address]?.[spenderAddress] ?? {}
useEffect(() => {
if (
isAddress(address) &&
isAddress(token?.address) &&
isAddress(token?.address) !== WETH[chainId].address &&
isAddress(spenderAddress) &&
(value === undefined || blockNumber !== globalBlockNumber) &&
(chainId || chainId === 0) &&
library
) {
let stale = false
getTokenAllowance(address, token?.address, spenderAddress, library)
.then(value => {
if (!stale) {
update(chainId, address, token?.address, spenderAddress, value, globalBlockNumber)
}
})
.catch(() => {
if (!stale) {
update(chainId, address, token?.address, spenderAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
}
}, [address, token, spenderAddress, value, blockNumber, globalBlockNumber, chainId, library, update])
return value ? new TokenAmount(token, value) : null
}
import React, { createContext, useContext, useReducer, useRef, useMemo, useCallback, useEffect, ReactNode } from 'react'
import { TokenAmount, Token, JSBI, WETH } from '@uniswap/sdk'
import { useAllPairs } from './Pairs'
import { useAllTokens } from './Tokens'
import { useBlockNumber } from './Application'
import { useWeb3React, useDebounce } from '../hooks'
import { getEtherBalance, getTokenBalance, isAddress } from '../utils'
import { useAllDummyPairs } from './LocalStorage'
const LOCAL_STORAGE_KEY = 'BALANCES'
const SHORT_BLOCK_TIMEOUT = (60 * 2) / 15 // in seconds, represented as a block number delta
......@@ -325,25 +325,26 @@ export function Updater() {
}, [chainId, account, blockNumber, allTokens, fetchBalance, batchUpdateAccount])
// ensure token balances for all exchanges
const allPairs = useAllPairs()
const allPairs = useAllDummyPairs()
useEffect(() => {
if (typeof chainId === 'number' && typeof blockNumber === 'number') {
Promise.all(
Object.keys(allPairs)
.filter(pairAddress => {
const token0 = allPairs[pairAddress].token0
const token1 = allPairs[pairAddress].token1
allPairs
.filter(pair => {
const token0Address = pair.token0.address
const token1Address = pair.token1.address
const pairAddress = pair.liquidityToken.address
const hasValueToken0 = !!stateRef.current?.[chainId]?.[pairAddress]?.[token0]?.value
const hasValueToken1 = !!stateRef.current?.[chainId]?.[pairAddress]?.[token1]?.value
const hasValueToken0 = !!stateRef.current?.[chainId]?.[pairAddress]?.[token0Address]?.value
const hasValueToken1 = !!stateRef.current?.[chainId]?.[pairAddress]?.[token1Address]?.value
const cachedFetchedAsOfToken0 = fetchedAsOfCache.current?.[chainId]?.[pairAddress]?.token0
const cachedFetchedAsOfToken1 = fetchedAsOfCache.current?.[chainId]?.[pairAddress]?.token1
const fetchedAsOfToken0 =
stateRef.current?.[chainId]?.[pairAddress]?.[token0]?.blockNumber ?? cachedFetchedAsOfToken0
stateRef.current?.[chainId]?.[pairAddress]?.[token0Address]?.blockNumber ?? cachedFetchedAsOfToken0
const fetchedAsOfToken1 =
stateRef.current?.[chainId]?.[pairAddress]?.[token1]?.blockNumber ?? cachedFetchedAsOfToken1
stateRef.current?.[chainId]?.[pairAddress]?.[token1Address]?.blockNumber ?? cachedFetchedAsOfToken1
// if there's no values, and they're not being fetched, we need to fetch!
if (
......@@ -366,9 +367,10 @@ export function Updater() {
return false
}
})
.map(async pairAddress => {
const token0 = allPairs[pairAddress].token0
const token1 = allPairs[pairAddress].token1
.map(async pair => {
const token0Address = pair.token0.address
const token1Address = pair.token1.address
const pairAddress = pair.liquidityToken.address
fetchedAsOfCache.current = {
...fetchedAsOfCache.current,
......@@ -376,21 +378,27 @@ export function Updater() {
...fetchedAsOfCache.current?.[chainId],
[pairAddress]: {
...fetchedAsOfCache.current?.[chainId]?.[pairAddress],
[token0]: blockNumber,
[token1]: blockNumber
[token0Address]: blockNumber,
[token1Address]: blockNumber
}
}
}
return Promise.all([
fetchBalance(pairAddress, token0),
fetchBalance(pairAddress, token1)
]).then(([valueToken0, valueToken1]) => ({ pairAddress, token0, token1, valueToken0, valueToken1 }))
fetchBalance(pairAddress, token0Address),
fetchBalance(pairAddress, token1Address)
]).then(([valueToken0, valueToken1]) => ({
pairAddress,
token0Address,
token1Address,
valueToken0,
valueToken1
}))
})
).then(results => {
batchUpdateExchanges(
chainId,
results.flatMap(result => [result.pairAddress, result.pairAddress]),
results.flatMap(result => [result.token0, result.token1]),
results.flatMap(result => [result.token0Address, result.token1Address]),
results.flatMap(result => [result.valueToken0, result.valueToken1]),
blockNumber
)
......@@ -401,7 +409,7 @@ export function Updater() {
return null
}
export function useAllBalances(): Array<TokenAmount> {
export function useAllBalances(): { [ownerAddress: string]: { [tokenAddress: string]: TokenAmount } } {
const { chainId } = useWeb3React()
const [state] = useBalancesContext()
......@@ -453,7 +461,7 @@ export function useAddressBalance(address: string, token: Token): TokenAmount |
const formattedValue = value && token && new TokenAmount(token, value)
useEffect(() => {
if (typeof chainId === 'number' && isAddress(address) && token && token.address && isAddress(token.address)) {
if (typeof chainId === 'number' && isAddress(address) && isAddress(token?.address)) {
startListening(chainId, address, token.address)
return () => {
stopListening(chainId, address, token.address)
......@@ -467,17 +475,19 @@ export function useAddressBalance(address: string, token: Token): TokenAmount |
export function useAccountLPBalances(account: string) {
const { chainId } = useWeb3React()
const [, { startListening, stopListening }] = useBalancesContext()
const allPairs = useAllPairs()
const pairs = useAllDummyPairs()
useEffect(() => {
Object.keys(allPairs).map(pairAddress => {
if (typeof chainId === 'number' && isAddress(account)) {
startListening(chainId, account, pairAddress)
const cleanupFunctions = []
pairs.forEach(pair => {
startListening(chainId, account, pair.liquidityToken.address)
cleanupFunctions.push(() => stopListening(chainId, account, pair.liquidityToken.address))
})
return () => {
stopListening(chainId, account, pairAddress)
cleanupFunctions.forEach(cleanupFunction => cleanupFunction())
}
}
return true
})
}, [account, allPairs, chainId, startListening, stopListening])
}, [chainId, account, pairs, startListening, stopListening])
}
import React, { createContext, useContext, useMemo, useCallback, useEffect, useState } from 'react'
import { Token } from '@uniswap/sdk'
import { Token, Pair, TokenAmount, JSBI, WETH, ChainId } from '@uniswap/sdk'
import { getTokenDecimals, getTokenSymbol, getTokenName, isAddress } from '../utils'
import { useWeb3React } from '@web3-react/core'
import { useAllTokens } from './Tokens'
enum LocalStorageKeys {
VERSION = 'version',
......@@ -9,7 +10,8 @@ enum LocalStorageKeys {
BETA_MESSAGE_DISMISSED = 'betaMessageDismissed',
MIGRATION_MESSAGE_DISMISSED = 'migrationMessageDismissed',
DARK_MODE = 'darkMode',
TOKENS = 'tokens'
TOKENS = 'tokens',
PAIRS = 'pairs'
}
function useLocalStorage<T, S = T>(
......@@ -39,31 +41,32 @@ function useLocalStorage<T, S = T>(
return [value, setValue]
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function serializeTokens(
tokens: Token[]
): { chainId: number; address: string; decimals: number; symbol: string; name: string }[] {
return tokens.map(token => ({
interface SerializedToken {
chainId: number
address: string
decimals: number
symbol: string
name: string
}
function serializeToken(token: Token): SerializedToken {
return {
chainId: token.chainId,
address: token.address,
decimals: token.decimals,
symbol: token.symbol,
name: token.name
}))
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function deserializeTokens(serializedTokens: ReturnType<typeof serializeTokens>): Token[] {
return serializedTokens.map(
serializedToken =>
new Token(
function deserializeToken(serializedToken: SerializedToken): Token {
return new Token(
serializedToken.chainId,
serializedToken.address,
serializedToken.decimals,
serializedToken.symbol,
serializedToken.name
)
)
}
const LocalStorageContext = createContext<[any, any]>([{}, {}])
......@@ -88,28 +91,29 @@ export default function Provider({ children }: { children: React.ReactNode }) {
LocalStorageKeys.DARK_MODE,
window?.matchMedia('(prefers-color-scheme: dark)')?.matches ? true : false
)
const [tokens, setTokens] = useLocalStorage<Token[], ReturnType<typeof serializeTokens>>(
LocalStorageKeys.TOKENS,
[],
{
serialize: serializeTokens,
deserialize: deserializeTokens
}
)
const [tokens, setTokens] = useLocalStorage<Token[], SerializedToken[]>(LocalStorageKeys.TOKENS, [], {
serialize: (tokens: Token[]) => tokens.map(serializeToken),
deserialize: (serializedTokens: SerializedToken[]) => serializedTokens.map(deserializeToken)
})
const [pairs, setPairs] = useLocalStorage<Token[][], SerializedToken[][]>(LocalStorageKeys.PAIRS, [], {
serialize: (nestedTokens: Token[][]) => nestedTokens.map(tokens => tokens.map(serializeToken)),
deserialize: (serializedNestedTokens: SerializedToken[][]) =>
serializedNestedTokens.map(serializedTokens => serializedTokens.map(deserializeToken))
})
return (
<LocalStorageContext.Provider
value={useMemo(
() => [
{ version, lastSaved, betaMessageDismissed, migrationMessageDismissed, darkMode, tokens },
{ version, lastSaved, betaMessageDismissed, migrationMessageDismissed, darkMode, tokens, pairs },
{
setVersion,
setLastSaved,
setBetaMessageDismissed,
setMigrationMessageDismissed,
setDarkMode,
setTokens
setTokens,
setPairs
}
],
[
......@@ -120,12 +124,14 @@ export default function Provider({ children }: { children: React.ReactNode }) {
darkMode,
tokens,
pairs,
setVersion,
setLastSaved,
setBetaMessageDismissed,
setMigrationMessageDismissed,
setDarkMode,
setTokens
setTokens,
setPairs
]
)}
>
......@@ -242,3 +248,78 @@ export function useLocalStorageTokens(): [
return [tokens, { fetchTokenByAddress, addToken, removeTokenByAddress }]
}
const ZERO = JSBI.BigInt(0)
export function useLocalStoragePairAdder(): (pair: Pair) => void {
const [, { setPairs }] = useLocalStorageContext()
return useCallback(
(pair: Pair) => {
setPairs(pairs =>
pairs
.filter(tokens => !(tokens[0].equals(pair.token0) && tokens[1].equals(pair.token1)))
.concat([[pair.token0, pair.token1]])
)
},
[setPairs]
)
}
const bases = [
...Object.values(WETH),
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
]
export function useAllDummyPairs(): Pair[] {
const { chainId } = useWeb3React()
const tokens = useAllTokens()
const generatedPairs: Pair[] = useMemo(
() =>
Object.values(tokens)
// select only tokens on the current chain
.filter(token => token.chainId === chainId)
.flatMap(token => {
// for each token on the current chain,
return (
bases
// loop through all the bases valid for the current chain,
.filter(base => base.chainId === chainId)
// to construct pairs of the given token with each base
.map(base => {
if (base.equals(token)) {
return null
} else {
return new Pair(new TokenAmount(base, ZERO), new TokenAmount(token, ZERO))
}
})
.filter(pair => !!pair)
)
}),
[tokens, chainId]
)
const [{ pairs }] = useLocalStorageContext()
const userPairs = useMemo(
() =>
pairs
.filter(tokens => tokens[0].chainId === chainId)
.map(tokens => new Pair(new TokenAmount(tokens[0], ZERO), new TokenAmount(tokens[1], ZERO))),
[pairs, chainId]
)
return useMemo(() => {
return (
generatedPairs
.concat(userPairs)
// filter out duplicate pairs
.filter((pair, i, concatenatedPairs) => {
const firstAppearance = concatenatedPairs.findIndex(
concatenatedPair =>
concatenatedPair.token0.equals(pair.token0) && concatenatedPair.token1.equals(pair.token1)
)
return i === firstAppearance
})
)
}, [generatedPairs, userPairs])
}
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect, useState } from 'react'
import { useAddressBalance } from './Balances'
import { useWeb3React, usePairContract } from '../hooks'
import { ALL_TOKENS } from './Tokens'
import { ChainId, WETH, Token, TokenAmount, Pair, JSBI } from '@uniswap/sdk'
const ADDRESSES_KEY = 'ADDRESSES_KEY'
const ENTITIES_KEY = 'ENTITIES_KEY'
const UPDATE = 'UPDATE'
const UPDATE_PAIR_ENTITY = 'UPDATE_PAIR_ENTITY'
const ALL_PAIRS: [Token, Token][] = [
[
ALL_TOKENS[ChainId.RINKEBY][WETH[ChainId.RINKEBY].address],
ALL_TOKENS[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'] //dai
],
[
ALL_TOKENS[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'], // dai
ALL_TOKENS[ChainId.RINKEBY]['0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44'] // mkr
]
]
const PAIR_MAP: {
[chainId: number]: { [token0Address: string]: { [token1Address: string]: string } }
} = ALL_PAIRS.reduce((pairMap, [tokenA, tokenB]) => {
const tokens: [Token, Token] = tokenA?.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
// ensure exchanges are unique
if (pairMap?.[tokens[0].chainId]?.[tokens[0].address]?.[tokens[1].address]?.address !== undefined)
throw Error(`Duplicate exchange: ${tokenA} ${tokenB}`)
return {
...pairMap,
[tokens[0].chainId]: {
...pairMap?.[tokens[0].chainId],
[ADDRESSES_KEY]: {
...pairMap?.[tokens[0].chainId]?.[ADDRESSES_KEY],
[tokens[0].address]: {
...pairMap?.[tokens[0].chainId]?.[ADDRESSES_KEY]?.[tokens[0].address],
[tokens[1].address]: Pair.getAddress(...tokens)
}
},
[ENTITIES_KEY]: {}
}
}
}, {})
const PairContext = createContext([])
function usePairContext() {
return useContext(PairContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case UPDATE: {
const { tokens } = payload
const tokensSorted: [Token, Token] = tokens[0].sortsBefore(tokens[1])
? [tokens[0], tokens[1]]
: [tokens[1], tokens[0]]
return {
...state,
[tokensSorted[0].chainId]: {
...state?.[tokensSorted[0].chainId],
[ADDRESSES_KEY]: {
...state?.[tokensSorted[0].chainId]?.[ADDRESSES_KEY],
[tokensSorted[0].address]: {
...state?.[tokensSorted[0].chainId]?.[ADDRESSES_KEY]?.[tokensSorted[0].address],
[tokensSorted[1].address]: Pair.getAddress(tokensSorted[0], tokensSorted[1])
}
}
}
}
}
case UPDATE_PAIR_ENTITY: {
const { pairAddress, pair, chainId } = payload
return {
...state,
[chainId]: {
...state?.[chainId],
[ENTITIES_KEY]: {
...state?.[chainId]?.[ENTITIES_KEY],
[pairAddress]: pair
}
}
}
}
default: {
throw Error(`Unexpected action type in ExchangesContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, PAIR_MAP)
const update = useCallback((chainId, tokens) => {
dispatch({ type: UPDATE, payload: { chainId, tokens } })
}, [])
const updatePairEntity = useCallback((pairAddress, pair, chainId) => {
dispatch({ type: UPDATE_PAIR_ENTITY, payload: { pairAddress, pair, chainId } })
}, [])
return (
<PairContext.Provider
value={useMemo(() => [state, { update, updatePairEntity }], [state, update, updatePairEntity])}
>
{children}
</PairContext.Provider>
)
}
export function usePairAddress(tokenA?: Token, tokenB?: Token): string | undefined {
const { chainId } = useWeb3React()
const [state, { update }] = usePairContext()
const tokens: [Token, Token] = tokenA && tokenB && tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
const address = state?.[chainId]?.[ADDRESSES_KEY]?.[tokens[0]?.address]?.[tokens[1]?.address]
useEffect(() => {
if (address === undefined && tokenA && tokenB) {
const pairAddress = Pair.getAddress(...tokens)
pairAddress && update(chainId, tokens)
}
}, [chainId, address, tokenA, tokenB, tokens, update])
return address
}
export function usePair(tokenA?: Token, tokenB?: Token): Pair | undefined {
const { chainId } = useWeb3React()
const [state, { updatePairEntity }] = usePairContext()
const address = usePairAddress(tokenA, tokenB)
const pair = state?.[chainId]?.[ENTITIES_KEY]?.[address]
const tokenAmountA = useAddressBalance(address, tokenA)
const tokenAmountB = useAddressBalance(address, tokenB)
useEffect(() => {
if (!pair && tokenAmountA && tokenAmountB) {
updatePairEntity(address, new Pair(tokenAmountA, tokenAmountB), chainId)
}
}, [pair, tokenAmountA, tokenAmountB, address, updatePairEntity, chainId])
return pair
}
export function useAllPairs() {
const { chainId } = useWeb3React()
const [state] = usePairContext()
const allPairDetails = state?.[chainId]?.[ADDRESSES_KEY]
const allPairs = useMemo(() => {
if (!allPairDetails) {
return {}
}
const formattedExchanges = {}
Object.keys(allPairDetails).map(token0Address => {
return Object.keys(allPairDetails[token0Address]).map(token1Address => {
const pairAddress = allPairDetails[token0Address][token1Address]
if (pairAddress) {
return (formattedExchanges[pairAddress] = {
token0: token0Address,
token1: token1Address
})
} else {
return null
}
})
})
return formattedExchanges
}, [allPairDetails])
return useMemo(() => {
return allPairs || {}
}, [allPairs])
}
export function useTotalSupply(tokenA?: Token, tokenB?: Token) {
const { library } = useWeb3React()
const pair = usePair(tokenA, tokenB)
const [totalPoolTokens, setTotalPoolTokens] = useState<TokenAmount>()
const pairContract = usePairContract(pair?.liquidityToken.address)
const fetchPoolTokens = useCallback(async () => {
!!pairContract &&
pairContract
.deployed()
.then(() => {
if (pairContract) {
pairContract.totalSupply().then(totalSupply => {
if (totalSupply !== undefined && pair?.liquidityToken?.decimals) {
const supplyFormatted = JSBI.BigInt(totalSupply)
const tokenSupplyFormatted = new TokenAmount(pair?.liquidityToken, supplyFormatted)
setTotalPoolTokens(tokenSupplyFormatted)
}
})
}
})
.catch(error => {
console.log(error)
})
}, [pairContract, pair])
// on the block make sure we're updated
useEffect(() => {
fetchPoolTokens()
library.on('block', fetchPoolTokens)
return () => {
library.removeListener('block', fetchPoolTokens)
}
}, [fetchPoolTokens, library])
return totalPoolTokens
}
......@@ -8,20 +8,31 @@ export const ALL_TOKENS = [
...Object.values(WETH),
// Mainnet Tokens
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker'),
// Rinkeby Tokens
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.RINKEBY, '0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44', 18, 'IANV2', 'IAn V2 /Coin'),
new Token(ChainId.RINKEBY, '0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85', 18, 'MKR', 'Maker'),
// Kovan Tokens
new Token(ChainId.KOVAN, '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.KOVAN, '0xAaF64BFCC32d0F15873a02163e7E500671a4ffcD', 18, 'MKR', 'Maker'),
// Ropsten Tokens
new Token(ChainId.ROPSTEN, '0xaD6D458402F60fD3Bd25163575031ACDce07538D', 18, 'DAI', 'Dai Stablecoin')
// Goerli Tokens
]
// remap WETH to ETH
.map(token => {
if (token.equals(WETH[token.chainId])) {
;(token as any).symbol = 'ETH'
;(token as any).name = 'Ether'
}
return token
})
// put into an object
.reduce((tokenMap, token) => {
if (tokenMap?.[token.chainId]?.[token.address] !== undefined) throw Error('Duplicate tokens.')
......@@ -39,11 +50,6 @@ export function useAllTokens(): { [address: string]: Token } {
const [localStorageTokens] = useLocalStorageTokens()
return useMemo(() => {
// rename WETH to ETH (in case not used in useToken yet)
if (ALL_TOKENS[chainId]?.[WETH[chainId]?.address]) {
ALL_TOKENS[chainId][WETH[chainId].address].name = 'ETH'
ALL_TOKENS[chainId][WETH[chainId].address].symbol = 'ETH'
}
return (
localStorageTokens
// filter to the current chain
......@@ -64,11 +70,5 @@ export function useToken(tokenAddress: string): Token {
const token = tokens?.[tokenAddress]
// rename WETH to ETH
if (token?.equals(WETH[token?.chainId])) {
;(token as any).symbol = 'ETH'
;(token as any).name = 'Ether'
}
return token
}
import { Contract } from '@ethersproject/contracts'
import { Token, TokenAmount } from '@uniswap/sdk'
import useSWR from 'swr'
import { SWRKeys } 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> =>
contract
.allowance(owner, spender)
.then((balance: { toString: () => string }) => new TokenAmount(token, balance.toString()))
}
export function useTokenAllowance(
token?: Token,
owner?: string,
spender?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): 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
}
)
return data
}
import { Contract } from '@ethersproject/contracts'
import { Token, TokenAmount, Pair } from '@uniswap/sdk'
import useSWR from 'swr'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { useContract } from '../hooks'
import { SWRKeys } from '.'
function getReserves(contract: Contract, token0: Token, token1: Token): () => Promise<Pair | null> {
return async (): Promise<Pair | null> =>
contract
.getReserves()
.then(
({ reserve0, reserve1 }: { reserve0: { toString: () => string }; reserve1: { toString: () => string } }) => {
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
}
)
}
// undefined while loading, null if no liquidity, pair otherwise
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 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
}
)
return data
}
import { Contract } from '@ethersproject/contracts'
import { Token, TokenAmount } from '@uniswap/sdk'
import useSWR from 'swr'
import { abi as IERC20ABI } from '@uniswap/v2-core/build/IERC20.json'
import { useContract } from '../hooks'
import { SWRKeys } from '.'
function getTotalSupply(contract: Contract, token: Token): () => Promise<TokenAmount> {
return async (): Promise<TokenAmount> =>
contract
.totalSupply()
.then((totalSupply: { toString: () => string }) => new TokenAmount(token, totalSupply.toString()))
}
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
}
)
return data
}
export enum SWRKeys {
Allowances,
Reserves,
TotalSupply
}
import { useMemo } from 'react'
import { WETH, Token, TokenAmount, Trade } from '@uniswap/sdk'
import { WETH, Token, TokenAmount, Trade, ChainId } from '@uniswap/sdk'
import { useWeb3React } from './index'
import { usePair } from '../contexts/Pairs'
import { isWETH } from '../utils'
import { usePair } from '../data/Reserves'
/**
* Returns the best trade for the exact amount of tokens in to the given token out
*/
const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade | null {
const inputToken = amountIn?.token
const outputToken = tokenOut
const { chainId } = useWeb3React()
// check for direct pair between tokens
const pairBetween = usePair(amountIn?.token, tokenOut)
const pairBetween = usePair(inputToken, outputToken)
// get token<->WETH pairs
const aToETH = usePair(amountIn && !isWETH(amountIn.token) ? amountIn.token : null, WETH[chainId])
const bToETH = usePair(tokenOut && !isWETH(tokenOut) ? tokenOut : null, WETH[chainId])
const aToETH = usePair(inputToken, WETH[chainId])
const bToETH = usePair(outputToken, WETH[chainId])
// get token<->DAI pairs
const aToDAI = usePair(inputToken, chainId === ChainId.MAINNET ? DAI : null)
const bToDAI = usePair(outputToken, chainId === ChainId.MAINNET ? DAI : null)
// get token<->USDC pairs
const aToUSDC = usePair(inputToken, chainId === ChainId.MAINNET ? USDC : null)
const bToUSDC = usePair(outputToken, chainId === ChainId.MAINNET ? USDC : null)
return useMemo(() => {
const allPairs = [pairBetween, aToETH, bToETH].filter(p => !!p)
const allPairs = [pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC].filter(p => !!p)
if (amountIn && allPairs.length > 0 && tokenOut) {
if (amountIn && tokenOut && allPairs.length > 0) {
try {
// TODO(moodysalem): remove when the insufficient reserves/input errors do not throw exceptions
return Trade.bestTradeExactIn(allPairs, amountIn, tokenOut)[0] ?? null
......@@ -29,26 +41,37 @@ export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade
}
}
return null
}, [aToETH, bToETH, pairBetween, amountIn, tokenOut])
}, [pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, amountIn, tokenOut])
}
/**
* Returns the best trade for the token in to the exact amount of token out
*/
export function useTradeExactOut(tokenIn?: Token, amountOut?: TokenAmount): Trade | null {
const inputToken = tokenIn
const outputToken = amountOut?.token
const { chainId } = useWeb3React()
// check for direct pair between tokens
const pairBetween = usePair(amountOut?.token, tokenIn)
// get token<->WETH pairs
const aToETH = usePair(amountOut && !isWETH(amountOut.token) ? amountOut.token : null, WETH[chainId])
const bToETH = usePair(tokenIn && !isWETH(tokenIn) ? tokenIn : null, WETH[chainId])
const aToETH = usePair(inputToken, WETH[chainId])
const bToETH = usePair(outputToken, WETH[chainId])
// get token<->DAI pairs
const aToDAI = usePair(inputToken, chainId === ChainId.MAINNET ? DAI : null)
const bToDAI = usePair(outputToken, chainId === ChainId.MAINNET ? DAI : null)
// get token<->USDC pairs
const aToUSDC = usePair(inputToken, chainId === ChainId.MAINNET ? USDC : null)
const bToUSDC = usePair(outputToken, chainId === ChainId.MAINNET ? USDC : null)
return useMemo(() => {
const allPairs = [pairBetween, aToETH, bToETH].filter(p => !!p)
const allPairs = [pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC].filter(p => !!p)
if (amountOut && allPairs.length > 0 && tokenIn) {
if (tokenIn && amountOut && allPairs.length > 0) {
try {
// TODO(moodysalem): remove when the insufficient reserves/input errors do not throw exceptions
return Trade.bestTradeExactOut(allPairs, tokenIn, amountOut)[0] ?? null
......@@ -57,5 +80,5 @@ export function useTradeExactOut(tokenIn?: Token, amountOut?: TokenAmount): Trad
}
}
return null
}, [pairBetween, aToETH, bToETH, amountOut, tokenIn])
}, [pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, tokenIn, amountOut])
}
......@@ -10,8 +10,6 @@ import LocalStorageContextProvider, { Updater as LocalStorageContextUpdater } fr
import ApplicationContextProvider, { Updater as ApplicationContextUpdater } from './contexts/Application'
import TransactionContextProvider, { Updater as TransactionContextUpdater } from './contexts/Transactions'
import BalancesContextProvider, { Updater as BalancesContextUpdater } from './contexts/Balances'
import ExchangesContextProvider from './contexts/Pairs'
import AllowancesContextProvider from './contexts/Allowances'
import App from './pages/App'
import ThemeProvider, { GlobalStyle } from './theme'
import './i18n'
......@@ -40,11 +38,7 @@ function ContextProviders({ children }: { children: React.ReactNode }) {
<LocalStorageContextProvider>
<ApplicationContextProvider>
<TransactionContextProvider>
<ExchangesContextProvider>
<BalancesContextProvider>
<AllowancesContextProvider>{children}</AllowancesContextProvider>
</BalancesContextProvider>
</ExchangesContextProvider>
<BalancesContextProvider>{children}</BalancesContextProvider>
</TransactionContextProvider>
</ApplicationContextProvider>
</LocalStorageContextProvider>
......
......@@ -2,9 +2,9 @@ import React, { useReducer, useState, useCallback, useEffect, useContext } from
import styled, { ThemeContext } from 'styled-components'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { parseUnits, parseEther } from '@ethersproject/units'
import { MaxUint256, Zero } from '@ethersproject/constants'
import { MaxUint256 } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts'
import { WETH, TokenAmount, JSBI, Percent, Route, Token, Pair, Price } from '@uniswap/sdk'
import { WETH, TokenAmount, JSBI, Percent, Route, Token, Price } from '@uniswap/sdk'
import TokenLogo from '../../components/TokenLogo'
import DoubleLogo from '../../components/DoubleLogo'
......@@ -22,14 +22,15 @@ import Row, { AutoRow, RowBetween, RowFlat, RowFixed } from '../../components/Ro
import { useToken } from '../../contexts/Tokens'
import { useAddressBalance } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import { usePair, useTotalSupply } from '../../contexts/Pairs'
import { useTokenAllowance } from '../../data/Allowances'
import { useTotalSupply } from '../../data/TotalSupply'
import { useWeb3React, useTokenContract } from '../../hooks'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
import { ROUTER_ADDRESS } from '../../constants'
import { getRouterContract, calculateGasMargin, isWETH } from '../../utils'
import { getRouterContract, calculateGasMargin, calculateSlippageAmount } from '../../utils'
import { BigNumber } from '@ethersproject/bignumber'
import { usePair } from '../../data/Reserves'
import { useLocalStorageTokens } from '../../contexts/LocalStorage'
import { useAllTokens } from '../../contexts/Tokens'
......@@ -213,22 +214,12 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
const tokenContractInput: Contract = useTokenContract(tokens[Field.INPUT]?.address)
const tokenContractOutput: Contract = useTokenContract(tokens[Field.OUTPUT]?.address)
// check on pending approvals for token amounts
const pendingApprovalInput = usePendingApproval(tokens[Field.INPUT]?.address)
const pendingApprovalOutput = usePendingApproval(tokens[Field.OUTPUT]?.address)
// exhchange data
const pair: Pair = usePair(tokens[Field.INPUT], tokens[Field.OUTPUT])
// exchange data
const pair = usePair(tokens[Field.INPUT], tokens[Field.OUTPUT])
const route: Route = pair ? new Route([pair], tokens[independentField]) : undefined
const totalSupply: TokenAmount = useTotalSupply(tokens[Field.INPUT], tokens[Field.OUTPUT])
const totalSupply: TokenAmount = useTotalSupply(pair?.liquidityToken)
const noLiquidity = // used to detect new exchange
pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0))
// state for amount approvals
const inputApproval: TokenAmount = useAddressAllowance(account, tokens[Field.INPUT], ROUTER_ADDRESS)
const outputApproval: TokenAmount = useAddressAllowance(account, tokens[Field.OUTPUT], ROUTER_ADDRESS)
const [showInputApprove, setShowInputApprove] = useState<boolean>(false)
const [showOutputApprove, setShowOutputApprove] = useState<boolean>(false)
!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0))
// get user-pecific and token-specific lookup data
const userBalances: { [field: number]: TokenAmount } = {
......@@ -295,11 +286,27 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
// get formatted amounts
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField]?.toSignificant(8) : ''
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField]?.toSignificant(6) : ''
}
// used for displaying approximate starting price in UI
// check whether the user has approved the router on both tokens
const inputApproval: TokenAmount = useTokenAllowance(tokens[Field.INPUT], account, ROUTER_ADDRESS)
const outputApproval: TokenAmount = useTokenAllowance(tokens[Field.OUTPUT], account, ROUTER_ADDRESS)
const inputApproved =
tokens[Field.INPUT]?.equals(WETH[chainId]) ||
(!!inputApproval &&
!!parsedAmounts[Field.INPUT] &&
JSBI.greaterThanOrEqual(inputApproval.raw, parsedAmounts[Field.INPUT].raw))
const outputApproved =
tokens[Field.OUTPUT]?.equals(WETH[chainId]) ||
(!!outputApproval &&
!!parsedAmounts[Field.OUTPUT] &&
JSBI.greaterThanOrEqual(outputApproval.raw, parsedAmounts[Field.OUTPUT].raw))
// check on pending approvals for token amounts
const pendingApprovalInput = usePendingApproval(tokens[Field.INPUT]?.address)
const pendingApprovalOutput = usePendingApproval(tokens[Field.OUTPUT]?.address)
// used for displaying approximate starting price in UI
const derivedPrice =
parsedAmounts[Field.INPUT] &&
parsedAmounts[Field.OUTPUT] &&
......@@ -362,8 +369,11 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
const [maxAmountInput, maxAmountOutput]: TokenAmount[] = [Field.INPUT, Field.OUTPUT].map(index => {
const field = Field[index]
return !!userBalances[Field[field]] &&
JSBI.greaterThan(userBalances[Field[field]].raw, isWETH(tokens[Field[field]]) ? MIN_ETHER.raw : JSBI.BigInt(0))
? isWETH(tokens[Field[field]])
JSBI.greaterThan(
userBalances[Field[field]].raw,
tokens[Field[field]]?.equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
)
? tokens[Field[field]]?.equals(WETH[chainId])
? userBalances[Field[field]].subtract(MIN_ETHER)
: userBalances[Field[field]]
: undefined
......@@ -377,18 +387,6 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
: undefined
})
// monitor parsed amounts and update approve buttons
useEffect(() => {
setShowInputApprove(
parsedAmounts[Field.INPUT] && inputApproval && JSBI.greaterThan(parsedAmounts[Field.INPUT].raw, inputApproval.raw)
)
setShowOutputApprove(
parsedAmounts[Field.OUTPUT] &&
outputApproval &&
JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, outputApproval.raw)
)
}, [inputApproval, outputApproval, parsedAmounts])
// errors
const [generalError, setGeneralError] = useState('')
const [inputError, setInputError] = useState('')
......@@ -402,6 +400,11 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
setOutputError(null)
setIsValid(true)
if (!account) {
setGeneralError('Connect Wallet')
setIsValid(false)
}
if (noLiquidity && parsedAmounts[Field.INPUT] && JSBI.equal(parsedAmounts[Field.INPUT].raw, JSBI.BigInt(0))) {
setGeneralError('Enter an amount')
setIsValid(false)
......@@ -436,57 +439,39 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
setOutputError('Insufficient ' + tokens[Field.OUTPUT]?.symbol + ' balance')
setIsValid(false)
}
}, [noLiquidity, parsedAmounts, showInputApprove, showOutputApprove, tokens, userBalances])
}, [noLiquidity, parsedAmounts, tokens, userBalances, account])
// state for txn
const addTransaction = useTransactionAdder()
const [txHash, setTxHash] = useState<string>('')
// format ETH value for transaction
function hex(value: JSBI) {
return BigNumber.from(value.toString())
}
// calculate slippage bounds based on current reserves
function calculateSlippageAmount(value: TokenAmount): JSBI[] {
if (value && value.raw) {
const offset = JSBI.divide(JSBI.multiply(JSBI.BigInt(ALLOWED_SLIPPAGE), value.raw), JSBI.BigInt(10000))
return [JSBI.subtract(value.raw, offset), JSBI.add(value.raw, offset)]
} else {
return null
}
}
async function onAdd() {
setAttemptingTxn(true)
const router = getRouterContract(chainId, library, account)
const minTokenInput = calculateSlippageAmount(parsedAmounts[Field.INPUT])[0]
const minTokenOutput = calculateSlippageAmount(parsedAmounts[Field.OUTPUT])[0]
const minInput = calculateSlippageAmount(parsedAmounts[Field.INPUT], ALLOWED_SLIPPAGE)[0]
const minOutput = calculateSlippageAmount(parsedAmounts[Field.OUTPUT], ALLOWED_SLIPPAGE)[0]
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
let method, estimate, args, value
if (tokens[Field.INPUT] === WETH[chainId] || tokens[Field.OUTPUT] === WETH[chainId]) {
// one of the tokens is ETH
if (tokens[Field.INPUT].equals(WETH[chainId]) || tokens[Field.OUTPUT].equals(WETH[chainId])) {
method = router.addLiquidityETH
estimate = router.estimateGas.addLiquidityETH
const outputIsETH = tokens[Field.OUTPUT].equals(WETH[chainId])
args = [
tokens[Field.OUTPUT] === WETH[chainId] ? tokens[Field.INPUT].address : tokens[Field.OUTPUT].address, // token
tokens[Field.OUTPUT] === WETH[chainId] // token desired
? parsedAmounts[Field.INPUT].raw.toString()
: parsedAmounts[Field.OUTPUT].raw.toString(),
tokens[Field.OUTPUT] === WETH[chainId] ? minTokenInput.toString() : minTokenOutput.toString(), // token min
tokens[Field.OUTPUT] === WETH[chainId] ? minTokenOutput.toString() : minTokenInput.toString(), // eth min
tokens[outputIsETH ? Field.INPUT : Field.OUTPUT].address, // token
parsedAmounts[outputIsETH ? Field.INPUT : Field.OUTPUT].raw.toString(), // token desired
outputIsETH ? minInput.toString() : minOutput.toString(), // token min
outputIsETH ? minOutput.toString() : minInput.toString(), // eth min
account,
deadline
]
value = hex(
tokens[Field.OUTPUT] === WETH[chainId] // eth desired
? parsedAmounts[Field.OUTPUT].raw
: parsedAmounts[Field.INPUT].raw
)
value = BigNumber.from(parsedAmounts[outputIsETH ? Field.OUTPUT : Field.INPUT].raw.toString())
} else {
method = router.addLiquidity
estimate = router.estimateGas.addLiquidity
......@@ -495,21 +480,19 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
tokens[Field.OUTPUT].address,
parsedAmounts[Field.INPUT].raw.toString(),
parsedAmounts[Field.OUTPUT].raw.toString(),
noLiquidity ? parsedAmounts[Field.INPUT].raw.toString() : minTokenInput.toString(),
noLiquidity ? parsedAmounts[Field.OUTPUT].raw.toString() : minTokenOutput.toString(),
noLiquidity ? parsedAmounts[Field.INPUT].raw.toString() : minInput.toString(),
noLiquidity ? parsedAmounts[Field.OUTPUT].raw.toString() : minOutput.toString(),
account,
deadline
]
value = Zero
value = null
}
await estimate(...args, {
value: value
})
await estimate(...args, value ? { value } : {})
.then(estimatedGasLimit =>
method(...args, {
gasLimit: calculateGasMargin(estimatedGasLimit),
value: value
...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => {
setTxHash(response.hash)
addTransaction(
......@@ -527,7 +510,7 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
})
)
.catch((e: Error) => {
console.log(e)
console.error(e)
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowConfirm(false)
......@@ -592,7 +575,7 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
<AutoColumn gap="20px">
<RowFlat style={{ marginTop: '20px' }}>
<Text fontSize="48px" fontWeight={500} lineHeight="32px" marginRight={10}>
{liquidityMinted?.toFixed(6)}
{liquidityMinted?.toSignificant(6)}
</Text>
<DoubleLogo a0={tokens[Field.INPUT]?.symbol || ''} a1={tokens[Field.OUTPUT]?.symbol || ''} size={30} />
</RowFlat>
......@@ -616,14 +599,14 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
<RowBetween>
<TYPE.body>{tokens[Field.INPUT]?.symbol} Deposited</TYPE.body>
<RowFixed>
<TokenLogo address={tokens[Field.INPUT]?.address || ''} style={{ marginRight: '8px' }} />
<TokenLogo address={tokens[Field.INPUT]?.address} style={{ marginRight: '8px' }} />
<TYPE.body>{!!parsedAmounts[Field.INPUT] && parsedAmounts[Field.INPUT].toSignificant(6)}</TYPE.body>
</RowFixed>
</RowBetween>
<RowBetween>
<TYPE.body>{tokens[Field.OUTPUT]?.symbol} Deposited</TYPE.body>
<RowFixed>
<TokenLogo address={tokens[Field.OUTPUT]?.address || ''} style={{ marginRight: '8px' }} />
<TokenLogo address={tokens[Field.OUTPUT]?.address} style={{ marginRight: '8px' }} />
<TYPE.body>{!!parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.OUTPUT].toSignificant(6)}</TYPE.body>
</RowFixed>
</RowBetween>
......@@ -633,13 +616,13 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
<TYPE.body>
{`1 ${tokens[Field.INPUT]?.symbol} = ${route?.midPrice &&
route?.midPrice?.raw?.denominator &&
route?.midPrice?.adjusted?.toFixed(8)} ${tokens[Field.OUTPUT]?.symbol}`}
route?.midPrice?.adjusted?.toSignificant(4)} ${tokens[Field.OUTPUT]?.symbol}`}
</TYPE.body>
</RowBetween>
)}
<RowBetween>
<TYPE.body>Minted Pool Share:</TYPE.body>
<TYPE.body>{noLiquidity ? '100%' : poolTokenPercentage?.toFixed(6) + '%'}</TYPE.body>
<TYPE.body>{noLiquidity ? '100%' : poolTokenPercentage?.toSignificant(6) + '%'}</TYPE.body>
</RowBetween>
<ButtonPrimary style={{ margin: '20px 0' }} onClick={onAdd}>
<Text fontWeight={500} fontSize={20}>
......@@ -696,7 +679,7 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
</AutoColumn>
<AutoColumn justify="center">
<TYPE.black>
{noLiquidity && derivedPrice ? '100' : poolTokenPercentage ? poolTokenPercentage?.toFixed(2) : '0.0'}
{noLiquidity && derivedPrice ? '100' : poolTokenPercentage?.toSignificant(4) ?? '0'}
{'%'}
</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
......@@ -791,28 +774,31 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
</GreyCard>
</>
)}
{showOutputApprove ? (
{isValid ? (
!inputApproved ? (
<ButtonLight
onClick={() => {
!pendingApprovalOutput && approveAmount(Field.OUTPUT)
approveAmount(Field.INPUT)
}}
disabled={pendingApprovalInput}
>
{pendingApprovalOutput ? (
<Dots>Approving {tokens[Field.OUTPUT]?.symbol}</Dots>
{pendingApprovalInput ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.OUTPUT]?.symbol
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonLight>
) : showInputApprove ? (
) : !outputApproved ? (
<ButtonLight
onClick={() => {
!pendingApprovalInput && approveAmount(Field.INPUT)
approveAmount(Field.OUTPUT)
}}
disabled={pendingApprovalOutput}
>
{pendingApprovalInput ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
{pendingApprovalOutput ? (
<Dots>Approving {tokens[Field.OUTPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
'Approve ' + tokens[Field.OUTPUT]?.symbol
)}
</ButtonLight>
) : (
......@@ -820,8 +806,14 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid}
>
<Text fontSize={20} fontWeight={500}>
Supply
</Text>
</ButtonPrimary>
)
) : (
<ButtonPrimary disabled={true}>
<Text fontSize={20} fontWeight={500}>
{generalError ? generalError : inputError ? inputError : outputError ? outputError : 'Supply'}
</Text>
......@@ -832,12 +824,7 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
{!noLiquidity && (
<FixedBottom>
<AutoColumn>
<PositionCard
pairAddress={pair?.liquidityToken?.address}
token0={tokens[Field.INPUT]}
token1={tokens[Field.OUTPUT]}
minimal={true}
/>
<PositionCard pair={pair} minimal={true} />
</AutoColumn>
</FixedBottom>
)}
......
import React, { useReducer, useState, useCallback, useEffect, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { parseUnits } from '@ethersproject/units'
import { Zero } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts'
import { TokenAmount, JSBI, Route, WETH, Percent, Token, Pair } from '@uniswap/sdk'
import { TokenAmount, JSBI, Route, WETH, Percent, Token } from '@uniswap/sdk'
import Slider from '../../components/Slider'
import TokenLogo from '../../components/TokenLogo'
......@@ -25,11 +24,15 @@ import { useWeb3React } from '../../hooks'
import { useAllBalances } from '../../contexts/Balances'
import { usePairContract } from '../../hooks'
import { useTransactionAdder } from '../../contexts/Transactions'
import { usePair, useTotalSupply } from '../../contexts/Pairs'
import { useTotalSupply } from '../../data/TotalSupply'
import { splitSignature } from '@ethersproject/bytes'
import { ROUTER_ADDRESS } from '../../constants'
import { getRouterContract, calculateGasMargin } from '../../utils'
import { getRouterContract, calculateGasMargin, calculateSlippageAmount } from '../../utils'
import { usePair } from '../../data/Reserves'
// denominated in bips
const ALLOWED_SLIPPAGE = 50
// denominated in seconds
const DEADLINE_FROM_NOW = 60 * 20
......@@ -165,14 +168,14 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
[Field.TOKEN1]: outputToken
}
const pair: Pair = usePair(inputToken, outputToken)
const pair = usePair(inputToken, outputToken)
const pairContract: Contract = usePairContract(pair?.liquidityToken.address)
// pool token data
const totalPoolTokens: TokenAmount = useTotalSupply(tokens[Field.TOKEN0], tokens[Field.TOKEN1])
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
const allBalances: TokenAmount[] = useAllBalances()
const userLiquidity: TokenAmount = allBalances?.[account]?.[pair?.liquidityToken?.address]
const allBalances = useAllBalances()
const userLiquidity = allBalances?.[account]?.[pair?.liquidityToken?.address]
// input state
const [state, dispatch] = useReducer(reducer, initializeRemoveState(userLiquidity?.toExact(), token0, token1))
......@@ -266,10 +269,10 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
pair.getLiquidityValue(tokens[Field.TOKEN1], totalPoolTokens, parsedAmounts[Field.LIQUIDITY], false)
// derived percent for advanced mode
const derivedPerecent =
userLiquidity &&
const derivedPercent =
parsedAmounts[Field.LIQUIDITY] &&
new Percent(parsedAmounts[Field.LIQUIDITY]?.raw, userLiquidity.raw).toFixed(0)
userLiquidity &&
new Percent(parsedAmounts[Field.LIQUIDITY]?.raw, userLiquidity.raw)
const [override, setSliderOverride] = useState(false) // override slider internal value
const handlePresetPercentage = newPercent => {
......@@ -299,19 +302,19 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
independentField === Field.LIQUIDITY
? typedValue
: parsedAmounts[Field.LIQUIDITY]
? parsedAmounts[Field.LIQUIDITY].toSignificant(8)
? parsedAmounts[Field.LIQUIDITY].toSignificant(6)
: '',
[Field.TOKEN0]:
independentField === Field.TOKEN0
? typedValue
: parsedAmounts[Field.TOKEN0]
? parsedAmounts[Field.TOKEN0].toSignificant(8)
? parsedAmounts[Field.TOKEN0].toSignificant(6)
: '',
[Field.TOKEN1]:
independentField === Field.TOKEN1
? typedValue
: parsedAmounts[Field.TOKEN1]
? parsedAmounts[Field.TOKEN1].toSignificant(8)
? parsedAmounts[Field.TOKEN1].toSignificant(6)
: ''
}
......@@ -439,18 +442,23 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
let method, args, estimate
// removal with ETH
if (tokens[Field.TOKEN0] === WETH[chainId] || tokens[Field.TOKEN1] === WETH[chainId]) {
if (tokens[Field.TOKEN0].equals(WETH[chainId]) || tokens[Field.TOKEN1].equals(WETH[chainId])) {
method = router.removeLiquidityETHWithPermit
estimate = router.estimateGas.removeLiquidityETHWithPermit
const token0IsETH = tokens[Field.TOKEN0].equals(WETH[chainId])
args = [
tokens[Field.TOKEN1] === WETH[chainId] ? tokens[Field.TOKEN0].address : tokens[Field.TOKEN1].address,
tokens[token0IsETH ? Field.TOKEN1 : Field.TOKEN0].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
tokens[Field.TOKEN1] === WETH[chainId]
? parsedAmounts[Field.TOKEN0].raw.toString()
: parsedAmounts[Field.TOKEN1].raw.toString(),
tokens[Field.TOKEN1] === WETH[chainId]
? parsedAmounts[Field.TOKEN1].raw.toString()
: parsedAmounts[Field.TOKEN0].raw.toString(),
calculateSlippageAmount(
parsedAmounts[token0IsETH ? Field.TOKEN1 : Field.TOKEN0],
ALLOWED_SLIPPAGE
)[0].toString(),
calculateSlippageAmount(
parsedAmounts[token0IsETH ? Field.TOKEN0 : Field.TOKEN1],
ALLOWED_SLIPPAGE
)[0].toString(),
account,
deadline,
false,
......@@ -467,8 +475,8 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
tokens[Field.TOKEN0].address,
tokens[Field.TOKEN1].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
parsedAmounts[Field.TOKEN0].raw.toString(),
parsedAmounts[Field.TOKEN1].raw.toString(),
calculateSlippageAmount(parsedAmounts[Field.TOKEN0], ALLOWED_SLIPPAGE)[0].toString(),
calculateSlippageAmount(parsedAmounts[Field.TOKEN1], ALLOWED_SLIPPAGE)[0].toString(),
account,
deadline,
false,
......@@ -478,9 +486,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
]
}
await estimate(...args, {
value: Zero
})
await estimate(...args)
.then(estimatedGasLimit =>
method(...args, {
gasLimit: calculateGasMargin(estimatedGasLimit)
......@@ -501,7 +507,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
})
)
.catch(e => {
console.log(e)
console.error(e)
resetModalState()
setShowConfirm(false)
})
......@@ -537,9 +543,9 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
</RowBetween>
<TYPE.italic fontSize={12} color={theme.text2} textAlign="left" padding={'12px 0 0 0'}>
{`Output is estimated. You will receive at least ${parsedAmounts[Field.TOKEN0]?.toFixed(6)} ${
{`Output is estimated. You will receive at least ${parsedAmounts[Field.TOKEN0]?.toSignificant(6)} ${
tokens[Field.TOKEN0]?.symbol
} and at least ${parsedAmounts[Field.TOKEN1]?.toFixed(6)} ${
} and at least ${parsedAmounts[Field.TOKEN1]?.toSignificant(6)} ${
tokens[Field.TOKEN1]?.symbol
} or the transaction will revert.`}
</TYPE.italic>
......@@ -570,7 +576,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
Price
</Text>
<Text fontWeight={500} fontSize={16} color={theme.text1}>
{`1 ${tokens[Field.TOKEN0]?.symbol} = ${route?.midPrice && route.midPrice.adjusted.toFixed(8)} ${
{`1 ${tokens[Field.TOKEN0]?.symbol} = ${route?.midPrice && route.midPrice.adjusted.toSignificant(6)} ${
tokens[Field.TOKEN1]?.symbol
}`}
</Text>
......@@ -633,11 +639,15 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
</RowBetween>
<Row style={{ alignItems: 'flex-end' }}>
<Text fontSize={72} fontWeight={500}>
{derivedPerecent ? (parseInt(derivedPerecent) < 1 ? '<1' : derivedPerecent) : '0'}%
{derivedPercent?.toFixed(0) === '0' ? '<1' : derivedPercent?.toFixed(0) ?? '0'}%
</Text>
</Row>
{!showAdvanced && (
<Slider value={parseFloat(derivedPerecent)} onChange={handleSliderChange} override={override} />
<Slider
value={parseInt(derivedPercent?.toFixed(0) ?? '0')}
onChange={handleSliderChange}
override={override}
/>
)}
{!showAdvanced && (
<RowBetween>
......@@ -669,7 +679,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
{formattedAmounts[Field.TOKEN0] ? formattedAmounts[Field.TOKEN0] : '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN0]?.address || ''} style={{ marginRight: '12px' }} />
<TokenLogo address={tokens[Field.TOKEN0]?.address} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500}>
{tokens[Field.TOKEN0]?.symbol}
</Text>
......@@ -680,7 +690,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
{formattedAmounts[Field.TOKEN1] ? formattedAmounts[Field.TOKEN1] : '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN1]?.address || ''} style={{ marginRight: '12px' }} />
<TokenLogo address={tokens[Field.TOKEN1]?.address} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500}>
{tokens[Field.TOKEN1]?.symbol}
</Text>
......@@ -759,12 +769,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
</Text>
</ButtonPrimary>
<FixedBottom>
<PositionCard
pairAddress={pair?.liquidityToken.address}
token0={pair?.token0}
token1={pair?.token1}
minimal={true}
/>
<PositionCard pair={pair} minimal={true} />
</FixedBottom>
</div>
</AutoColumn>
......
import React, { useState, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { JSBI } from '@uniswap/sdk'
import { JSBI, Pair } from '@uniswap/sdk'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import Question from '../../components/Question'
......@@ -13,10 +13,10 @@ import { RowBetween } from '../../components/Row'
import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { useAllPairs } from '../../contexts/Pairs'
import { useWeb3React } from '@web3-react/core'
import { useAllTokens } from '../../contexts/Tokens'
import { useAllBalances, useAccountLPBalances } from '../../contexts/Balances'
import { usePair } from '../../data/Reserves'
import { useAllDummyPairs } from '../../contexts/LocalStorage'
const Positions = styled.div`
position: relative;
......@@ -29,36 +29,33 @@ const FixedBottom = styled.div`
width: 100%;
`
function PositionCardWrapper({ dummyPair }: { dummyPair: Pair }) {
const pair = usePair(dummyPair.token0, dummyPair.token1)
return <PositionCard pair={pair} />
}
function Supply({ history }: RouteComponentProps) {
const theme = useContext(ThemeContext)
const { account } = useWeb3React()
const [showPoolSearch, setShowPoolSearch] = useState(false)
const allTokens = useAllTokens()
const allBalances = useAllBalances()
const allPairs = useAllPairs()
const theme = useContext(ThemeContext)
// initiate listener for LP balances
const allBalances = useAllBalances()
useAccountLPBalances(account)
const filteredExchangeList = Object.keys(allPairs)
.filter(pairAddress => {
const pairs = useAllDummyPairs()
const filteredExchangeList = pairs
.filter(pair => {
return (
allBalances &&
allBalances[account] &&
allBalances[account][pairAddress] &&
JSBI.greaterThan(allBalances[account][pairAddress].raw, JSBI.BigInt(0))
allBalances[account][pair.liquidityToken.address] &&
JSBI.greaterThan(allBalances[account][pair.liquidityToken.address].raw, JSBI.BigInt(0))
)
})
.map((pairAddress, i) => {
return (
<PositionCard
key={i}
pairAddress={pairAddress}
token0={allTokens[allPairs[pairAddress].token0]}
token1={allTokens[allPairs[pairAddress].token1]}
/>
)
.map((pair, i) => {
return <PositionCardWrapper key={i} dummyPair={pair} />
})
return (
......
import { getEtherscanLink } from './index'
import { AddressZero } from '@ethersproject/constants'
import { TokenAmount, Token, ChainId } from '@uniswap/sdk'
import { getEtherscanLink, calculateSlippageAmount } from '.'
describe('utils', () => {
describe('#getEtherscanLink', () => {
......@@ -9,4 +12,16 @@ describe('utils', () => {
expect(getEtherscanLink(1, 'abc', 'address')).toEqual('https://etherscan.io/address/abc')
})
})
describe('#calculateSlippageAmount', () => {
it('bounds are correct', () => {
const tokenAmount = new TokenAmount(new Token(ChainId.MAINNET, AddressZero, 0), '100')
expect(() => calculateSlippageAmount(tokenAmount, -1)).toThrow()
expect(calculateSlippageAmount(tokenAmount, 0).map(bound => bound.toString())).toEqual(['100', '100'])
expect(calculateSlippageAmount(tokenAmount, 100).map(bound => bound.toString())).toEqual(['99', '101'])
expect(calculateSlippageAmount(tokenAmount, 200).map(bound => bound.toString())).toEqual(['98', '102'])
expect(calculateSlippageAmount(tokenAmount, 10000).map(bound => bound.toString())).toEqual(['0', '200'])
expect(() => calculateSlippageAmount(tokenAmount, 10001)).toThrow()
})
})
})
......@@ -3,7 +3,6 @@ import { getAddress } from '@ethersproject/address'
import { AddressZero } from '@ethersproject/constants'
import { parseBytes32String } from '@ethersproject/strings'
import { BigNumber } from '@ethersproject/bignumber'
import { WETH } from '@uniswap/sdk'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { abi as IUniswapV2Router01ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router01.json'
......@@ -11,6 +10,7 @@ import { ROUTER_ADDRESS, SUPPORTED_THEMES } from '../constants'
import ERC20_ABI from '../constants/abis/erc20.json'
import ERC20_BYTES32_ABI from '../constants/abis/erc20_bytes32.json'
import { JSBI, TokenAmount } from '@uniswap/sdk'
export function isAddress(value: any): string | false {
try {
......@@ -106,6 +106,16 @@ export function calculateGasMargin(value: BigNumber): BigNumber {
return value.mul(BigNumber.from(10000).add(BigNumber.from(1000))).div(BigNumber.from(10000)) // add 10%
}
export function calculateSlippageAmount(value: TokenAmount, slippage: number): [JSBI, JSBI] {
if (slippage < 0 || slippage > 10000) {
throw Error(`Unexpected slippage value: ${slippage}`)
}
return [
JSBI.divide(JSBI.multiply(value.raw, JSBI.BigInt(10000 - slippage)), JSBI.BigInt(10000)),
JSBI.divide(JSBI.multiply(value.raw, JSBI.BigInt(10000 + slippage)), JSBI.BigInt(10000))
]
}
// account is optional
export function getProviderOrSigner(library: any, account?: string): any {
return account ? library.getSigner(account).connectUnchecked() : library
......@@ -197,23 +207,3 @@ export async function getTokenBalance(tokenAddress, address, library) {
return getContract(tokenAddress, ERC20_ABI, library).balanceOf(address)
}
// get the token allowance
export async function getTokenAllowance(address, tokenAddress, spenderAddress, library) {
if (!isAddress(address) || !isAddress(tokenAddress) || !isAddress(spenderAddress)) {
throw Error(
"Invalid 'address' or 'tokenAddress' or 'spenderAddress' parameter" +
`'${address}' or '${tokenAddress}' or '${spenderAddress}'.`
)
}
return getContract(tokenAddress, ERC20_ABI, library).allowance(address, spenderAddress)
}
export function isWETH(token) {
if (token && token.address === WETH[token.chainId].address) {
return true
} else {
return false
}
}
import { Signer } from 'ethers'
import { JsonRpcSigner, Provider } from 'ethers/providers'
import { TransactionResponse } from 'ethers/providers/abstract-provider'
/**
* Wraps a JsonRpcSigner and replaces `sendTransaction` with `sendUncheckedTransaction`
*/
export default class UncheckedJsonRpcSigner extends Signer {
private readonly signer: JsonRpcSigner
public readonly provider: Provider
constructor(signer: JsonRpcSigner) {
super()
this.signer = signer
this.provider = signer.provider
}
getAddress(): Promise<string> {
return this.signer.getAddress()
}
sendTransaction(transaction): Promise<TransactionResponse> {
return this.signer.sendUncheckedTransaction(transaction).then(hash => {
return {
hash,
nonce: null,
gasLimit: null,
gasPrice: null,
data: null,
value: null,
chainId: null,
confirmations: 0,
from: null,
wait: confirmations => {
return this.signer.provider.waitForTransaction(hash, confirmations)
}
}
})
}
signMessage(message) {
return this.signer.signMessage(message)
}
}
......@@ -4982,9 +4982,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001039, caniuse-lite@^1.0.30001043:
version "1.0.30001053"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001053.tgz#b7ae027567ce2665b965b0437e4512b296ccd20d"
integrity sha512-HtV4wwIZl6GA4Oznse8aR274XUOYGZnQLcf/P8vHgmlfqSNelwD+id8CyHOceqLqt9yfKmo7DUZTh1EuS9pukg==
version "1.0.30001054"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001054.tgz#7e82fc42d927980b0ce1426c4813df12381e1a75"
integrity sha512-jiKlTI6Ur8Kjfj8z0muGrV6FscpRvefcQVPSuMuXnvRCfExU7zlVLNjmOz1TnurWgUrAY7MMmjyy+uTgIl1XHw==
capture-exit@^2.0.0:
version "2.0.0"
......@@ -6602,9 +6602,9 @@ ee-first@1.1.1:
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.413, electron-to-chromium@^1.3.47:
version "1.3.430"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.430.tgz#33914f7c2db771bdcf30977bd4fd6258ee8a2f37"
integrity sha512-HMDYkANGhx6vfbqpOf/hc6hWEmiOipOHGDeRDeUb3HLD3XIWpvKQxFgWf0tgHcr3aNv6I/8VPecplqmQsXoZSw==
version "1.3.431"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.431.tgz#705dd8ef46200415ba837b31d927cdc1e43db303"
integrity sha512-2okqkXCIda7qDwjYhUFxPcQdZDIZZ/zBLDzVOif7WW/TSNfEhdT6SO07O1x/sFteEHX189Z//UwjbZKKCOn2Fg==
elegant-spinner@^1.0.1:
version "1.0.1"
......@@ -7843,7 +7843,7 @@ fancy-log@^1.3.2:
parse-node-version "^1.0.0"
time-stamp "^1.0.0"
fast-deep-equal@^2.0.1:
fast-deep-equal@2.0.1, fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
......@@ -15931,6 +15931,13 @@ swarm-js@0.1.39:
tar "^4.0.2"
xhr-request-promise "^0.1.2"
swr@0.1.18:
version "0.1.18"
resolved "https://registry.yarnpkg.com/swr/-/swr-0.1.18.tgz#be62df4cb8d188dc092305b35ecda1f3be8e61c1"
integrity sha512-lD31JxsD0bXdT7dyGVIB7MHcwgFp+HbBBOLt075hJT0sEgW01E3+EuCeB6fsavxZ2UjUZ3f+SbNMo9c8pv9uiA==
dependencies:
fast-deep-equal "2.0.1"
symbol-observable@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment