Token check (#610)

* update market rate calculation and input parsing for 0 decimal tokens

* remove style bump and revert tokens

* add custom warning for pasted tokens

* update centering

* upload dark theme

* update copy and loading state

* copy edits

* update color

* multiple cards
This commit is contained in:
Ian Lapham 2020-01-24 17:24:53 -05:00 committed by GitHub
parent 8290cf565f
commit e05be71ea5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 323 additions and 15 deletions

@ -18,7 +18,7 @@ import Modal from '../Modal'
import TokenLogo from '../TokenLogo'
import SearchIcon from '../../assets/images/magnifying-glass.svg'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
import { useTokenDetails, useAllTokenDetails } from '../../contexts/Tokens'
import { useTokenDetails, useAllTokenDetails, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
import { useAddressBalance } from '../../contexts/Balances'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { transparentize } from 'polished'
@ -244,6 +244,10 @@ const TokenFullName = styled.div`
color: ${({ theme }) => theme.chaliceGray};
`
const FadedSpan = styled.span`
color: ${({ theme }) => theme.royalBlue};
`
const TokenRowBalance = styled.div`
font-size: 1rem;
line-height: 20px;
@ -284,7 +288,8 @@ export default function CurrencyInputPanel({
disableTokenSelect,
selectedTokenAddress = '',
showUnlock,
value
value,
urlAddedTokens
}) {
const { t } = useTranslation()
@ -431,6 +436,7 @@ export default function CurrencyInputPanel({
onDismiss={() => {
setModalIsOpen(false)
}}
urlAddedTokens={urlAddedTokens}
onTokenSelect={onCurrencySelected}
allBalances={allBalances}
/>
@ -439,7 +445,7 @@ export default function CurrencyInputPanel({
)
}
function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect, urlAddedTokens }) {
const { t } = useTranslation()
const [searchQuery, setSearchQuery] = useState('')
@ -447,7 +453,7 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
const allTokens = useAllTokenDetails()
const { account } = useWeb3React()
const { account, chainId } = useWeb3React()
// BigNumber.js instance
const ethPrice = useETHPriceInUSD()
@ -589,13 +595,25 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
}
return filteredTokenList.map(({ address, symbol, name, balance, usdBalance }) => {
const urlAdded = urlAddedTokens && urlAddedTokens.hasOwnProperty(address)
const customAdded =
address !== 'ETH' &&
INITIAL_TOKENS_CONTEXT[chainId] &&
!INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(address) &&
!urlAdded
return (
<TokenModalRow key={address} onClick={() => _onTokenSelect(address)}>
<TokenRowLeft>
<TokenLogo address={address} size={'2rem'} />
<TokenSymbolGroup>
<span id="symbol">{symbol}</span>
<TokenFullName>{name}</TokenFullName>
<div>
<span id="symbol">{symbol}</span>
<FadedSpan>
{urlAdded && '(Added by URL)'} {customAdded && '(Added by user)'}
</FadedSpan>
</div>
<TokenFullName> {name}</TokenFullName>
</TokenSymbolGroup>
</TokenRowLeft>
<TokenRowRight>

@ -10,7 +10,7 @@ import { brokenTokens } from '../../constants'
import { amountFormatter, calculateGasMargin } from '../../utils'
import { useExchangeContract } from '../../hooks'
import { useTokenDetails } from '../../contexts/Tokens'
import { useTokenDetails, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
@ -23,6 +23,7 @@ import AddressInputPanel from '../AddressInputPanel'
import OversizedPanel from '../OversizedPanel'
import TransactionDetails from '../TransactionDetails'
import ArrowDown from '../../assets/svg/SVGArrowDown'
import WarningCard from '../WarningCard'
const INPUT = 0
const OUTPUT = 1
@ -248,7 +249,15 @@ function getMarketRate(
export default function ExchangePage({ initialCurrency, sending = false, params }) {
const { t } = useTranslation()
const { account, error } = useWeb3React()
const { account, chainId, error } = useWeb3React()
const urlAddedTokens = {}
if (params.inputCurrency) {
urlAddedTokens[params.inputCurrency] = true
}
if (params.outputCurrency) {
urlAddedTokens[params.outputCurrency] = true
}
// BigNumber.js instance
const ethPrice = useETHPriceInUSD()
@ -710,10 +719,54 @@ export default function ExchangePage({ initialCurrency, sending = false, params
const toggleWalletModal = useWalletModalToggle()
const newInputDetected =
inputCurrency !== 'ETH' && inputCurrency && !INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(inputCurrency)
const newOutputDetected =
outputCurrency !== 'ETH' && outputCurrency && !INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(outputCurrency)
const [showInputWarning, setShowInputWarning] = useState(false)
const [showOutputWarning, setShowOutputWarning] = useState(false)
useEffect(() => {
if (newInputDetected) {
setShowInputWarning(true)
} else {
setShowInputWarning(false)
}
}, [newInputDetected, setShowInputWarning])
useEffect(() => {
if (newOutputDetected) {
setShowOutputWarning(true)
} else {
setShowOutputWarning(false)
}
}, [newOutputDetected, setShowOutputWarning])
return (
<>
{showInputWarning && (
<WarningCard
onDismiss={() => {
setShowInputWarning(false)
}}
urlAddedTokens={urlAddedTokens}
currency={inputCurrency}
/>
)}
{showOutputWarning && (
<WarningCard
onDismiss={() => {
setShowOutputWarning(false)
}}
urlAddedTokens={urlAddedTokens}
currency={outputCurrency}
/>
)}
<CurrencyInputPanel
title={t('input')}
urlAddedTokens={urlAddedTokens}
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
extraTextClickHander={() => {
@ -764,6 +817,7 @@ export default function ExchangePage({ initialCurrency, sending = false, params
title={t('output')}
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
urlAddedTokens={urlAddedTokens}
onCurrencySelected={outputCurrency => {
dispatchSwapState({
type: 'SELECT_CURRENCY',

@ -0,0 +1,180 @@
import React, { useState } from 'react'
import styled, { keyframes } from 'styled-components'
import { useWeb3React } from '../../hooks'
import { useTokenDetails } from '../../contexts/Tokens'
import { getEtherscanLink } from '../../utils'
import { Link } from '../../theme'
import TokenLogo from '../TokenLogo'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import question from '../../assets/images/question.svg'
const Flex = styled.div`
display: flex;
justify-content: center;
padding: 2rem;
button {
max-width: 20rem;
}
`
const Wrapper = styled.div`
background: rgba(243, 190, 30, 0.1);
position: relative;
padding: 1rem;
border: 0.5px solid #f3be1e;
border-radius: 10px;
margin-bottom: 20px;
display: grid;
grid-template-rows: 1fr 1fr 1fr;
grid-row-gap: 10px;
`
const Row = styled.div`
display: flex;
align-items: center;
justify-items: flex-start;
& > * {
margin-right: 6px;
}
`
const CloseColor = styled(Close)`
color: #aeaeae;
`
const CloseIcon = styled.div`
position: absolute;
right: 1rem;
top: 14px;
&:hover {
cursor: pointer;
opacity: 0.6;
}
& > * {
height: 14px;
width: 14px;
}
`
const QuestionWrapper = styled.button`
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
margin-left: 0.4rem;
padding: 0.2rem;
border: none;
background: none;
outline: none;
cursor: default;
border-radius: 36px;
:hover,
:focus {
opacity: 0.7;
}
`
const HelpCircleStyled = styled.img`
height: 18px;
width: 18px;
`
const fadeIn = keyframes`
from {
opacity : 0;
}
to {
opacity : 1;
}
`
const Popup = styled(Flex)`
position: absolute;
width: 228px;
right: 110px;
top: 4px;
z-index: 10;
flex-direction: column;
align-items: center;
padding: 0.6rem 1rem;
line-height: 150%;
background: ${({ theme }) => theme.backgroundColor};
border: 1px solid ${({ theme }) => theme.mercuryGray};
border-radius: 8px;
animation: ${fadeIn} 0.15s linear;
color: ${({ theme }) => theme.textColor};
font-style: italic;
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.04), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.04);
${({ theme }) => theme.mediaWidth.upToSmall`
left: 2px;
top: 50px;
`}
`
const Text = styled.div`
color: ${({ theme }) => theme.textColor};
`
function WarningCard({ onDismiss, urlAddedTokens, currency }) {
const [showPopup, setPopup] = useState()
const { chainId } = useWeb3React()
const { symbol: inputSymbol, name: inputName } = useTokenDetails(currency)
const fromURL = urlAddedTokens.hasOwnProperty(currency)
return (
<Wrapper>
<CloseIcon onClick={() => onDismiss()}>
<CloseColor alt={'close icon'} />
</CloseIcon>
<Row style={{ fontSize: '12px' }}>
<Text>{fromURL ? 'Token imported by URL ' : 'Token imported by user'}</Text>
<QuestionWrapper
onClick={() => {
setPopup(!showPopup)
}}
onMouseEnter={() => {
setPopup(true)
}}
onMouseLeave={() => {
setPopup(false)
}}
>
<HelpCircleStyled src={question} alt="popup" />
</QuestionWrapper>
{showPopup ? (
<Popup>
<Text>
The Uniswap smart contracts are designed to support any ERC20 token on Ethereum. Any token can be loaded
into the interface by entering its Ethereum address into the search field or passing it as a URL
parameter. Be careful when interacting with imported tokens as they have not been verified.
</Text>
</Popup>
) : (
''
)}
</Row>
<Row>
<TokenLogo address={currency} />
<div style={{ fontWeight: 500 }}>{inputName && inputSymbol ? inputName + ' (' + inputSymbol + ')' : ''}</div>
<Link style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, currency, 'address')}>
(View on Etherscan)
</Link>
</Row>
<Row style={{ fontSize: '12px', fontStyle: 'italic' }}>
<Text>Please verify the legitimacy of this token before making any transactions.</Text>
</Row>
</Wrapper>
)
}
export default WarningCard

@ -26,7 +26,7 @@ const ETH = {
}
}
const INITIAL_TOKENS_CONTEXT = {
export const INITIAL_TOKENS_CONTEXT = {
1: {
'0xB6eD7644C69416d67B522e20bC294A9a9B405B31': {
[NAME]: '0xBitcoin Token',
@ -662,7 +662,6 @@ export function useTokenDetails(tokenAddress) {
library
) {
let stale = false
const namePromise = getTokenName(tokenAddress, library).catch(() => null)
const symbolPromise = getTokenSymbol(tokenAddress, library).catch(() => null)
const decimalsPromise = getTokenDecimals(tokenAddress, library).catch(() => null)

@ -10,11 +10,13 @@ import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import OversizedPanel from '../../components/OversizedPanel'
import ContextualInfo from '../../components/ContextualInfo'
import { ReactComponent as Plus } from '../../assets/images/plus-blue.svg'
import WarningCard from '../../components/WarningCard'
import { useWeb3React, useExchangeContract } from '../../hooks'
import { brokenTokens } from '../../constants'
import { amountFormatter, calculateGasMargin } from '../../utils'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useTokenDetails } from '../../contexts/Tokens'
import { useTokenDetails, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
import { useAddressBalance, useExchangeReserves, useETHPriceInUSD } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
@ -198,11 +200,16 @@ function getMarketRate(reserveETH, reserveToken, decimals, invert = false) {
export default function AddLiquidity({ params }) {
const { t } = useTranslation()
const { library, account, active } = useWeb3React()
const { library, account, active, chainId } = useWeb3React()
// BigNumber.js instance
const ethPrice = useETHPriceInUSD()
const urlAddedTokens = {}
if (params.token) {
urlAddedTokens[params.token] = true
}
// clear url of query
useEffect(() => {
const history = createBrowserHistory()
@ -578,6 +585,18 @@ export default function AddLiquidity({ params }) {
const isValid =
(inputError === null || outputError === null) && !zeroDecimalError && !showUnlock && !brokenTokenWarning
const newOutputDetected =
outputCurrency !== 'ETH' && outputCurrency && !INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(outputCurrency)
const [showOutputWarning, setShowOutputWarning] = useState(false)
useEffect(() => {
if (newOutputDetected) {
setShowOutputWarning(true)
} else {
setShowOutputWarning(false)
}
}, [newOutputDetected, setShowOutputWarning])
return (
<>
{isNewExchange ? (
@ -591,7 +610,15 @@ export default function AddLiquidity({ params }) {
<NewExchangeWarningText>{t('initialExchangeRate', { symbol })}</NewExchangeWarningText>
</NewExchangeWarning>
) : null}
{showOutputWarning && (
<WarningCard
onDismiss={() => {
setShowOutputWarning(false)
}}
urlAddedTokens={urlAddedTokens}
currency={outputCurrency}
/>
)}
<CurrencyInputPanel
title={t('deposit')}
extraText={inputBalance && formatBalance(amountFormatter(inputBalance, 18, 4))}
@ -625,6 +652,7 @@ export default function AddLiquidity({ params }) {
extraText={
outputBalance && decimals && formatBalance(amountFormatter(outputBalance, decimals, Math.min(decimals, 4)))
}
urlAddedTokens={urlAddedTokens}
selectedTokenAddress={outputCurrency}
onCurrencySelected={outputCurrency => {
dispatchAddLiquidityState({ type: 'SELECT_CURRENCY', payload: outputCurrency })

@ -7,7 +7,7 @@ import styled from 'styled-components'
import { useWeb3React, useExchangeContract } from '../../hooks'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useTokenDetails } from '../../contexts/Tokens'
import { useTokenDetails, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
import { useAddressBalance, useETHPriceInUSD } from '../../contexts/Balances'
import { calculateGasMargin, amountFormatter } from '../../utils'
@ -18,6 +18,7 @@ import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel'
import ArrowDown from '../../assets/svg/SVGArrowDown'
import WarningCard from '../../components/WarningCard'
// denominated in bips
const ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(200)
@ -144,7 +145,7 @@ function calculateSlippageBounds(value) {
export default function RemoveLiquidity({ params }) {
const { t } = useTranslation()
const { library, account, active } = useWeb3React()
const { library, account, active, chainId } = useWeb3React()
const addTransaction = useTransactionAdder()
@ -194,6 +195,11 @@ export default function RemoveLiquidity({ params }) {
const exchangeETHBalance = useAddressBalance(exchangeAddress, 'ETH')
const exchangeTokenBalance = useAddressBalance(exchangeAddress, outputCurrency)
const urlAddedTokens = {}
if (params.poolTokenAddress) {
urlAddedTokens[params.poolTokenAddress] = true
}
// input validation
useEffect(() => {
if (valueParsed && poolTokenBalance) {
@ -355,8 +361,30 @@ export default function RemoveLiquidity({ params }) {
const marketRate = getMarketRate(exchangeETHBalance, exchangeTokenBalance, decimals)
const newOutputDetected =
outputCurrency !== 'ETH' && outputCurrency && !INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(outputCurrency)
const [showCustomTokenWarning, setShowCustomTokenWarning] = useState(false)
useEffect(() => {
if (newOutputDetected) {
setShowCustomTokenWarning(true)
} else {
setShowCustomTokenWarning(false)
}
}, [newOutputDetected])
return (
<>
{showCustomTokenWarning && (
<WarningCard
onDismiss={() => {
setShowCustomTokenWarning(false)
}}
urlAddedTokens={urlAddedTokens}
currency={outputCurrency}
/>
)}
<CurrencyInputPanel
title={t('poolTokens')}
extraText={poolTokenBalance && formatBalance(amountFormatter(poolTokenBalance, 18, 4))}
@ -368,6 +396,7 @@ export default function RemoveLiquidity({ params }) {
}
}
}}
urlAddedTokens={urlAddedTokens}
onCurrencySelected={setOutputCurrency}
onValueChange={setValue}
value={value}