Commit d9434a1a authored by cartcrom's avatar cartcrom Committed by GitHub

feat: update token safety / lists / verification (#4968)

* removed selected list logic and state
* updated copy
* updated warning color
* updated lists and fixed native currency bug
* removed no-longer-relevant active list tests
* removed leftover list code
* copy and color changes
parent a920a93b
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx'
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety'
import { getTokenDetailsURL } from 'graphql/data/util'
import uriToHttp from 'lib/utils/uriToHttp'
import { Box } from 'nft/components/Box'
......@@ -87,7 +88,6 @@ export const CollectionRow = ({
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{collection.name}</Box>
{collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />}
</Row>
<Box className={styles.secondaryText}>{putCommas(collection.stats.total_supply)} items</Box>
</Column>
......@@ -181,7 +181,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, traceE
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box>
{token.onDefaultList && <VerifiedIcon className={styles.suggestionIcon} />}
<TokenSafetyIcon warning={checkWarning(token.address)} />
</Row>
<Box className={styles.secondaryText}>{token.symbol}</Box>
</Column>
......
......@@ -2,7 +2,25 @@
exports[`renders currency rows correctly when currencies list is non-empty 1`] = `
<DocumentFragment>
.c7 {
.c9 {
color: #99A1BD;
}
.c7 {
margin-left: 4px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.c8 {
width: 1em;
height: 1em;
color: #99A1BD;
}
......@@ -55,7 +73,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
justify-content: space-between;
}
.c8 {
.c10 {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
......@@ -111,9 +129,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
>
Dai Stablecoin
</div>
<div
class="c7"
>
<svg
class="c8"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
<div
class="c7 css-1j6a53a"
class="c9 css-1j6a53a"
>
DAI
</div>
......@@ -122,7 +172,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
class="c4"
>
<div
class="c0 c1 c8"
class="c0 c1 c10"
style="justify-self: flex-end;"
/>
</div>
......@@ -150,9 +200,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
>
USD//C
</div>
<div
class="c7"
>
<svg
class="c8"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
<div
class="c7 css-1j6a53a"
class="c9 css-1j6a53a"
>
USDC
</div>
......@@ -161,7 +243,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
class="c4"
>
<div
class="c0 c1 c8"
class="c0 c1 c10"
style="justify-self: flex-end;"
/>
</div>
......@@ -189,9 +271,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
>
Wrapped BTC
</div>
<div
class="c7"
>
<svg
class="c8"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
<div
class="c7 css-1j6a53a"
class="c9 css-1j6a53a"
>
WBTC
</div>
......@@ -200,7 +314,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
class="c4"
>
<div
class="c0 c1 c8"
class="c0 c1 c10"
style="justify-self: flex-end;"
/>
</div>
......
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
......@@ -15,10 +14,8 @@ import styled from 'styled-components/macro'
import { useIsUserAddedToken } from '../../../hooks/Tokens'
import { useCurrencyBalance } from '../../../state/connection/hooks'
import { useCombinedActiveList } from '../../../state/lists/hooks'
import { WrappedTokenInfo } from '../../../state/lists/wrappedTokenInfo'
import { ThemedText } from '../../../theme'
import { isTokenOnList } from '../../../utils'
import Column, { AutoColumn } from '../../Column'
import CurrencyLogo from '../../CurrencyLogo'
import Loader from '../../Loader'
......@@ -128,8 +125,6 @@ export function CurrencyRow({
}) {
const { account } = useWeb3React()
const key = currencyKey(currency)
const selectedTokenList = useCombinedActiveList()
const isOnSelectedList = isTokenOnList(selectedTokenList, currency.isToken ? currency : undefined)
const customAdded = useIsUserAddedToken(currency)
const balance = useCurrencyBalance(account ?? undefined, currency)
const warning = currency.isNative ? null : checkWarning(currency.address)
......@@ -170,11 +165,7 @@ export function CurrencyRow({
{isBlockedToken && <BlockedTokenIcon />}
</Row>
<ThemedText.DeprecatedDarkGray ml="0px" fontSize={'12px'} fontWeight={300}>
{!currency.isNative && !isOnSelectedList && customAdded ? (
<Trans>{currency.symbol} • Added by user</Trans>
) : (
currency.symbol
)}
{currency.symbol}
</ThemedText.DeprecatedDarkGray>
</AutoColumn>
<Column>
......
......@@ -19,7 +19,7 @@ import { Text } from 'rebass'
import { useAllTokenBalances } from 'state/connection/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { useAllTokens, useIsUserAddedToken, useSearchInactiveTokenLists, useToken } from '../../hooks/Tokens'
import { useActiveTokens, useIsUserAddedToken, useSearchInactiveTokenLists, useToken } from '../../hooks/Tokens'
import { CloseIcon, ThemedText } from '../../theme'
import { isAddress } from '../../utils'
import Column from '../Column'
......@@ -71,7 +71,7 @@ export function CurrencySearch({
const [searchQuery, setSearchQuery] = useState<string>('')
const debouncedQuery = useDebounce(searchQuery, 200)
const allTokens = useAllTokens()
const allTokens = useActiveTokens()
// if they input an address, use it
const isAddressSearch = isAddress(debouncedQuery)
......
import { Currency, Token } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists'
import TokenSafety from 'components/TokenSafety'
import usePrevious from 'hooks/usePrevious'
import { memo, useCallback, useEffect, useState } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { useUserAddedTokens } from 'state/user/hooks'
import useLast from '../../hooks/useLast'
import { useWindowSize } from '../../hooks/useWindowSize'
import Modal from '../Modal'
import { CurrencySearch } from './CurrencySearch'
import { ImportList } from './ImportList'
import { ImportToken } from './ImportToken'
import Manage from './Manage'
interface CurrencySearchModalProps {
isOpen: boolean
......@@ -27,9 +21,7 @@ interface CurrencySearchModalProps {
export enum CurrencyModalView {
search,
manage,
importToken,
importList,
tokenSafety,
}
......@@ -69,25 +61,9 @@ export default memo(function CurrencySearchModal({
},
[onDismiss, onCurrencySelect, userAddedTokens]
)
// for token import view
const prevView = usePrevious(modalView)
// used for import token flow
const [importToken, setImportToken] = useState<Token | undefined>()
// used for import list
const [importList, setImportList] = useState<TokenList | undefined>()
const [listURL, setListUrl] = useState<string | undefined>()
// used for token safety
const [warningToken, setWarningToken] = useState<Token | undefined>()
const handleBackImport = useCallback(
() => setModalView(prevView && prevView !== CurrencyModalView.importToken ? prevView : CurrencyModalView.search),
[setModalView, prevView]
)
const { height: windowHeight } = useWindowSize()
// change min height if not searching
let modalHeight: number | undefined = 80
......@@ -124,38 +100,6 @@ export default memo(function CurrencySearchModal({
)
}
break
case CurrencyModalView.importToken:
if (importToken) {
modalHeight = undefined
showTokenSafetySpeedbump(importToken)
content = (
<ImportToken
tokens={[importToken]}
onDismiss={onDismiss}
list={importToken instanceof WrappedTokenInfo ? importToken.list : undefined}
onBack={handleBackImport}
handleCurrencySelect={handleCurrencySelect}
/>
)
}
break
case CurrencyModalView.importList:
modalHeight = 40
if (importList && listURL) {
content = <ImportList list={importList} listURL={listURL} onDismiss={onDismiss} setModalView={setModalView} />
}
break
case CurrencyModalView.manage:
content = (
<Manage
onDismiss={onDismiss}
setModalView={setModalView}
setImportToken={setImportToken}
setImportList={setImportList}
setListUrl={setListUrl}
/>
)
break
}
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={modalHeight} minHeight={modalHeight}>
......
import { Trans } from '@lingui/macro'
import { TokenList } from '@uniswap/token-lists'
import { sendEvent } from 'components/analytics'
import { ButtonPrimary } from 'components/Button'
import Card from 'components/Card'
import { AutoColumn } from 'components/Column'
import ListLogo from 'components/ListLogo'
import { AutoRow, RowBetween, RowFixed } from 'components/Row'
import { SectionBreak } from 'components/swap/styleds'
import { useFetchListCallback } from 'hooks/useFetchListCallback'
import { transparentize } from 'polished'
import { useCallback, useState } from 'react'
import { AlertTriangle, ArrowLeft } from 'react-feather'
import { useAppDispatch } from 'state/hooks'
import { enableList, removeList } from 'state/lists/actions'
import { useAllLists } from 'state/lists/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { CloseIcon, ThemedText } from 'theme'
import { ExternalLink } from '../../theme'
import { CurrencyModalView } from './CurrencySearchModal'
import { Checkbox, PaddedColumn, TextDot } from './styleds'
const Wrapper = styled.div`
position: relative;
width: 100%;
overflow: auto;
`
interface ImportProps {
listURL: string
list: TokenList
onDismiss: () => void
setModalView: (view: CurrencyModalView) => void
}
export function ImportList({ listURL, list, setModalView, onDismiss }: ImportProps) {
const theme = useTheme()
const dispatch = useAppDispatch()
// user must accept
const [confirmed, setConfirmed] = useState(false)
const lists = useAllLists()
const fetchList = useFetchListCallback()
// monitor is list is loading
const adding = Boolean(lists[listURL]?.loadingRequestId)
const [addError, setAddError] = useState<string | null>(null)
const handleAddList = useCallback(() => {
if (adding) return
setAddError(null)
fetchList(listURL)
.then(() => {
sendEvent({
category: 'Lists',
action: 'Add List',
label: listURL,
})
// turn list on
dispatch(enableList(listURL))
// go back to lists
setModalView(CurrencyModalView.manage)
})
.catch((error) => {
sendEvent({
category: 'Lists',
action: 'Add List Failed',
label: listURL,
})
setAddError(error.message)
dispatch(removeList(listURL))
})
}, [adding, dispatch, fetchList, listURL, setModalView])
return (
<Wrapper>
<PaddedColumn gap="14px" style={{ width: '100%', flex: '1 1' }}>
<RowBetween>
<ArrowLeft style={{ cursor: 'pointer' }} onClick={() => setModalView(CurrencyModalView.manage)} />
<ThemedText.DeprecatedMediumHeader>
<Trans>Import List</Trans>
</ThemedText.DeprecatedMediumHeader>
<CloseIcon onClick={onDismiss} />
</RowBetween>
</PaddedColumn>
<SectionBreak />
<PaddedColumn gap="md">
<AutoColumn gap="md">
<Card backgroundColor={theme.deprecated_bg2} padding="12px 20px">
<RowBetween>
<RowFixed>
{list.logoURI && <ListLogo logoURI={list.logoURI} size="40px" />}
<AutoColumn gap="sm" style={{ marginLeft: '20px' }}>
<RowFixed>
<ThemedText.DeprecatedBody fontWeight={600} mr="6px">
{list.name}
</ThemedText.DeprecatedBody>
<TextDot />
<ThemedText.DeprecatedMain fontSize={'16px'} ml="6px">
<Trans>{list.tokens.length} tokens</Trans>
</ThemedText.DeprecatedMain>
</RowFixed>
<ExternalLink href={`https://tokenlists.org/token-list?url=${listURL}`}>
<ThemedText.DeprecatedMain fontSize={'12px'} color={theme.deprecated_blue1}>
{listURL}
</ThemedText.DeprecatedMain>
</ExternalLink>
</AutoColumn>
</RowFixed>
</RowBetween>
</Card>
<Card style={{ backgroundColor: transparentize(0.8, theme.deprecated_red1) }}>
<AutoColumn justify="center" style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
<AlertTriangle stroke={theme.deprecated_red1} size={32} />
<ThemedText.DeprecatedBody fontWeight={500} fontSize={20} color={theme.deprecated_red1}>
<Trans>Import at your own risk</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
<AutoColumn style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
<ThemedText.DeprecatedBody fontWeight={500} color={theme.deprecated_red1}>
<Trans>
By adding this list you are implicitly trusting that the data is correct. Anyone can create a list,
including creating fake versions of existing lists and lists that claim to represent projects that do
not have one.
</Trans>
</ThemedText.DeprecatedBody>
<ThemedText.DeprecatedBody fontWeight={600} color={theme.deprecated_red1}>
<Trans>If you purchase a token from this list, you may not be able to sell it back.</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
<AutoRow justify="center" style={{ cursor: 'pointer' }} onClick={() => setConfirmed(!confirmed)}>
<Checkbox
name="confirmed"
type="checkbox"
checked={confirmed}
onChange={() => setConfirmed(!confirmed)}
/>
<ThemedText.DeprecatedBody ml="10px" fontSize="16px" color={theme.deprecated_red1} fontWeight={500}>
<Trans>I understand</Trans>
</ThemedText.DeprecatedBody>
</AutoRow>
</Card>
<ButtonPrimary
disabled={!confirmed}
altDisabledStyle={true}
$borderRadius="20px"
padding="10px 1rem"
onClick={handleAddList}
>
<Trans>Import</Trans>
</ButtonPrimary>
{addError ? (
<ThemedText.DeprecatedError title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
{addError}
</ThemedText.DeprecatedError>
) : null}
</AutoColumn>
{/* </Card> */}
</PaddedColumn>
</Wrapper>
)
}
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists'
import { RowBetween } from 'components/Row'
import { useState } from 'react'
import { ArrowLeft } from 'react-feather'
import { Text } from 'rebass'
import styled from 'styled-components/macro'
import { CloseIcon } from 'theme'
import { CurrencyModalView } from './CurrencySearchModal'
import { ManageLists } from './ManageLists'
import ManageTokens from './ManageTokens'
import { PaddedColumn, Separator } from './styleds'
const Wrapper = styled.div`
width: 100%;
position: relative;
display: flex;
flex-flow: column;
`
const ToggleWrapper = styled(RowBetween)`
background-color: ${({ theme }) => theme.deprecated_bg3};
border-radius: 12px;
padding: 6px;
`
const ToggleOption = styled.div<{ active?: boolean }>`
width: 48%;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
font-weight: 600;
background-color: ${({ theme, active }) => (active ? theme.deprecated_bg1 : theme.deprecated_bg3)};
color: ${({ theme, active }) => (active ? theme.deprecated_text1 : theme.deprecated_text2)};
user-select: none;
:hover {
cursor: pointer;
opacity: 0.7;
}
`
export default function Manage({
onDismiss,
setModalView,
setImportList,
setImportToken,
setListUrl,
}: {
onDismiss: () => void
setModalView: (view: CurrencyModalView) => void
setImportToken: (token: Token) => void
setImportList: (list: TokenList) => void
setListUrl: (url: string) => void
}) {
// toggle between tokens and lists
const [showLists, setShowLists] = useState(true)
return (
<Wrapper>
<PaddedColumn>
<RowBetween>
<ArrowLeft style={{ cursor: 'pointer' }} onClick={() => setModalView(CurrencyModalView.search)} />
<Text fontWeight={500} fontSize={20}>
<Trans>Manage</Trans>
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
</PaddedColumn>
<Separator />
<PaddedColumn style={{ paddingBottom: 0 }}>
<ToggleWrapper>
<ToggleOption onClick={() => setShowLists(!showLists)} active={showLists}>
<Trans>Lists</Trans>
</ToggleOption>
<ToggleOption onClick={() => setShowLists(!showLists)} active={!showLists}>
<Trans>Tokens</Trans>
</ToggleOption>
</ToggleWrapper>
</PaddedColumn>
{showLists ? (
<ManageLists setModalView={setModalView} setImportList={setImportList} setListUrl={setListUrl} />
) : (
<ManageTokens setModalView={setModalView} setImportToken={setImportToken} />
)}
</Wrapper>
)
}
This diff is collapsed.
import { ReactComponent as Verified } from 'assets/svg/verified.svg'
import { Warning } from 'constants/tokenSafety'
import { Warning, WARNING_LEVEL } from 'constants/tokenSafety'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
const VerifiedContainer = styled.div`
......@@ -8,17 +8,17 @@ const VerifiedContainer = styled.div`
justify-content: center;
`
export const VerifiedIcon = styled(Verified)<{ size?: string }>`
export const WarningIcon = styled(AlertTriangle)<{ size?: string }>`
width: ${({ size }) => size ?? '1em'};
height: ${({ size }) => size ?? '1em'};
color: ${({ theme }) => theme.accentAction};
color: ${({ theme }) => theme.textTertiary};
`
export default function TokenSafetyIcon({ warning }: { warning: Warning | null }) {
if (warning) return null
if (warning?.level !== WARNING_LEVEL.UNKNOWN) return null
return (
<VerifiedContainer>
<VerifiedIcon />
<WarningIcon />
</VerifiedContainer>
)
}
......@@ -9,7 +9,7 @@ import { Color } from 'theme/styled'
const Label = styled.div<{ color: Color }>`
width: 100%;
padding: 12px 20px;
padding: 12px 20px 16px;
background-color: ${({ color }) => color + '1F'};
border-radius: 16px;
color: ${({ color }) => color};
......@@ -31,9 +31,15 @@ const Title = styled(Text)`
const DetailsRow = styled.div`
margin-top: 8px;
font-size: 12px;
line-height: 16px;
color: ${({ theme }) => theme.textSecondary};
`
const StyledLink = styled(ExternalLink)`
color: ${({ theme }) => theme.textSecondary};
font-weight: 700;
`
type TokenWarningMessageProps = {
warning: Warning
tokenAddress: string
......@@ -56,9 +62,9 @@ export default function TokenWarningMessage({ warning, tokenAddress }: TokenWarn
{description}
{Boolean(description) && ' '}
{tokenAddress && (
<ExternalLink href={TOKEN_SAFETY_ARTICLE}>
<StyledLink href={TOKEN_SAFETY_ARTICLE}>
<Trans>Learn more</Trans>
</ExternalLink>
</StyledLink>
)}
</DetailsRow>
</Label>
......
......@@ -244,6 +244,11 @@ export default function TokenSafety({
}
const { heading, description } = getWarningCopy(displayWarning, plural)
const learnMoreUrl = (
<StyledExternalLink href={TOKEN_SAFETY_ARTICLE}>
<Trans>Learn more</Trans>
</StyledExternalLink>
)
return (
displayWarning && (
......@@ -255,13 +260,9 @@ export default function TokenSafety({
<ShortColumn>
<SafetyLabel warning={displayWarning} />
</ShortColumn>
<ShortColumn>{heading && <InfoText fontSize="20px">{heading}</InfoText>}</ShortColumn>
<ShortColumn>
<InfoText>
{description}{' '}
<StyledExternalLink href={TOKEN_SAFETY_ARTICLE}>
<Trans>Learn more</Trans>
</StyledExternalLink>
{heading} {description} {learnMoreUrl}
</InfoText>
</ShortColumn>
<LinkColumn>{urls}</LinkColumn>
......
......@@ -2,9 +2,7 @@ import { Trans } from '@lingui/macro'
import { Currency, NativeCurrency, Token } from '@uniswap/sdk-core'
import { ParentSize } from '@visx/responsive'
import CurrencyLogo from 'components/CurrencyLogo'
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety'
import { PriceDurations, PricePoint, SingleTokenData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod } from 'graphql/data/util'
......@@ -80,7 +78,6 @@ export default function ChartSection({
}) {
const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
const warning = checkWarning(token.address ?? '')
const timePeriod = useAtomValue(filterTimeAtom)
const logoSrc = useTokenLogoURI(token, nativeCurrency)
......@@ -120,7 +117,6 @@ export default function ChartSection({
</LogoContainer>
{nativeCurrency?.name ?? token.name ?? <Trans>Name not found</Trans>}
<TokenSymbol>{nativeCurrency?.symbol ?? token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
{!warning && <VerifiedIcon size="16px" />}
</TokenNameCell>
<TokenActions>
{token.name && token.symbol && token.address && <ShareButton token={token} isNative={!!nativeCurrency} />}
......
export const UNI_LIST = 'https://tokens.uniswap.org'
export const UNI_EXTENDED_LIST = 'https://extendedtokens.uniswap.org/'
const UNI_UNSUPPORTED_LISTS = 'https://unsupportedtokens.uniswap.org/'
const UNI_UNSUPPORTED_LIST = 'https://unsupportedtokens.uniswap.org/'
const AAVE_LIST = 'tokenlist.aave.eth'
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
const CMC_ALL_LIST = 'https://api.coinmarketcap.com/data-api/v3/uniswap/all.json'
......@@ -16,12 +16,11 @@ export const OPTIMISM_LIST = 'https://static.optimism.io/optimism.tokenlist.json
export const ARBITRUM_LIST = 'https://bridge.arbitrum.io/token-list-42161.json'
export const CELO_LIST = 'https://celo-org.github.io/celo-token-list/celo.tokenlist.json'
export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST, UNI_UNSUPPORTED_LISTS]
export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST, UNI_UNSUPPORTED_LIST]
// this is the default list of lists that are exposed to users
// lower index == higher priority for token import
const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [
UNI_LIST,
// default lists to be 'active' aka searched across
export const DEFAULT_ACTIVE_LIST_URLS: string[] = [UNI_LIST]
export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
UNI_EXTENDED_LIST,
COMPOUND_LIST,
AAVE_LIST,
......@@ -37,10 +36,11 @@ const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [
CELO_LIST,
]
// this is the default list of lists that are exposed to users
// lower index == higher priority for token import
const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [...DEFAULT_ACTIVE_LIST_URLS, ...DEFAULT_INACTIVE_LIST_URLS]
export const DEFAULT_LIST_OF_LISTS: string[] = [
...DEFAULT_LIST_OF_LISTS_TO_DISPLAY,
...UNSUPPORTED_LIST_URLS, // need to load dynamic unsupported tokens as well
]
// default lists to be 'active' aka searched across
export const DEFAULT_ACTIVE_LIST_URLS: string[] = [UNI_LIST, GEMINI_LIST]
import { Plural, Trans } from '@lingui/macro'
import { ZERO_ADDRESS } from './misc'
import { NATIVE_CHAIN_ID } from './tokens'
import WarningCache, { TOKEN_LIST_TYPES } from './TokenSafetyLookupTable'
export const TOKEN_SAFETY_ARTICLE = 'https://support.uniswap.org/hc/en-us/articles/8723118437133'
......@@ -14,10 +16,28 @@ export function getWarningCopy(warning: Warning | null, plural = false) {
let heading = null,
description = null
if (warning) {
if (warning.canProceed) {
heading = <Plural value={plural ? 2 : 1} _1="This token isn't verified." other="These tokens aren't verified." />
description = <Trans>Please do your own research before trading.</Trans>
} else {
switch (warning.level) {
case WARNING_LEVEL.MEDIUM:
heading = (
<Plural
value={plural ? 2 : 1}
_1="This token isn't traded on leading U.S. centralized exchanges."
other="These tokens aren't traded on leading U.S. centralized exchanges."
/>
)
description = <Trans>Always conduct your own research before trading.</Trans>
break
case WARNING_LEVEL.UNKNOWN:
heading = (
<Plural
value={plural ? 2 : 1}
_1="This token isn't traded on leading U.S. centralized exchanges or frequently swapped on Uniswap."
other="These tokens aren't traded on leading U.S. centralized exchanges or frequently swapped on Uniswap."
/>
)
description = <Trans>Always conduct your own research before trading.</Trans>
break
case WARNING_LEVEL.BLOCKED:
description = (
<Plural
value={plural ? 2 : 1}
......@@ -25,6 +45,7 @@ export function getWarningCopy(warning: Warning | null, plural = false) {
other="You can't trade these tokens using the Uniswap App."
/>
)
break
}
}
return { heading, description }
......@@ -57,6 +78,9 @@ const BlockedWarning: Warning = {
}
export function checkWarning(tokenAddress: string) {
if (tokenAddress === NATIVE_CHAIN_ID || tokenAddress === ZERO_ADDRESS) {
return null
}
switch (WarningCache.checkToken(tokenAddress.toLowerCase())) {
case TOKEN_LIST_TYPES.UNI_DEFAULT:
return null
......
......@@ -2,12 +2,13 @@ import { Currency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { DEFAULT_INACTIVE_LIST_URLS } from 'constants/lists'
import { useCurrencyFromMap, useTokenFromMapOrNetwork } from 'lib/hooks/useCurrency'
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
import { useMemo } from 'react'
import { isL2ChainId } from 'utils/chains'
import { useAllLists, useCombinedActiveList, useInactiveListUrls } from '../state/lists/hooks'
import { useAllLists, useCombinedActiveList } from '../state/lists/hooks'
import { WrappedTokenInfo } from '../state/lists/wrappedTokenInfo'
import { useUserAddedTokens, useUserAddedTokensOnChain } from '../state/user/hooks'
import { TokenAddressMap, useUnsupportedTokenList } from './../state/lists/hooks'
......@@ -54,6 +55,11 @@ export function useAllTokens(): { [address: string]: Token } {
return useTokensFromMap(allTokens, true)
}
export function useActiveTokens(): { [address: string]: Token } {
const allTokens = useCombinedActiveList()
return useTokensFromMap(allTokens, false)
}
type BridgeInfo = Record<
SupportedChainId,
{
......@@ -109,7 +115,7 @@ export function useUnsupportedTokens(): { [address: string]: Token } {
export function useSearchInactiveTokenLists(search: string | undefined, minResults = 10): WrappedTokenInfo[] {
const lists = useAllLists()
const inactiveUrls = useInactiveListUrls()
const inactiveUrls = DEFAULT_INACTIVE_LIST_URLS
const { chainId } = useWeb3React()
const activeTokens = useAllTokens()
return useMemo(() => {
......
......@@ -16,7 +16,7 @@ function balanceComparator(a?: CurrencyAmount<Currency>, b?: CurrencyAmount<Curr
type TokenBalances = { [tokenAddress: string]: CurrencyAmount<Token> | undefined }
/** Sorts tokens by currency amount (descending), then symbol (ascending). */
/** Sorts tokens by currency amount (descending), then safety, then symbol (ascending). */
export function tokenComparator(balances: TokenBalances, a: Token, b: Token) {
// Sorts by balances
const balanceComparison = balanceComparator(balances[a.address], balances[b.address])
......
......@@ -14,9 +14,5 @@ export const fetchTokenList: Readonly<{
export const addList = createAction<string>('lists/addList')
export const removeList = createAction<string>('lists/removeList')
// select which lists to search across from loaded lists
export const enableList = createAction<string>('lists/enableList')
export const disableList = createAction<string>('lists/disableList')
// versioning
export const acceptListUpdate = createAction<string>('lists/acceptListUpdate')
......@@ -5,7 +5,7 @@ import sortByListPriority from 'utils/listSort'
import BROKEN_LIST from '../../constants/tokenLists/broken.tokenlist.json'
import { AppState } from '../index'
import { UNSUPPORTED_LIST_URLS } from './../../constants/lists'
import { DEFAULT_ACTIVE_LIST_URLS, UNSUPPORTED_LIST_URLS } from './../../constants/lists'
export type TokenAddressMap = ChainTokenMap
......@@ -66,25 +66,9 @@ function useCombinedTokenMapFromUrls(urls: string[] | undefined): TokenAddressMa
}, [lists, urls])
}
// filter out unsupported lists
export function useActiveListUrls(): string[] | undefined {
const activeListUrls = useAppSelector((state) => state.lists.activeListUrls)
return useMemo(() => activeListUrls?.filter((url) => !UNSUPPORTED_LIST_URLS.includes(url)), [activeListUrls])
}
export function useInactiveListUrls(): string[] {
const lists = useAllLists()
const allActiveListUrls = useActiveListUrls()
return useMemo(
() => Object.keys(lists).filter((url) => !allActiveListUrls?.includes(url) && !UNSUPPORTED_LIST_URLS.includes(url)),
[lists, allActiveListUrls]
)
}
// get all the tokens from active lists, combine with local default tokens
export function useCombinedActiveList(): TokenAddressMap {
const activeListUrls = useActiveListUrls()
const activeTokens = useCombinedTokenMapFromUrls(activeListUrls)
const activeTokens = useCombinedTokenMapFromUrls(DEFAULT_ACTIVE_LIST_URLS)
return activeTokens
}
......@@ -100,6 +84,5 @@ export function useUnsupportedTokenList(): TokenAddressMap {
return useMemo(() => combineMaps(brokenListMap, loadedUnsupportedListMap), [brokenListMap, loadedUnsupportedListMap])
}
export function useIsListActive(url: string): boolean {
const activeListUrls = useActiveListUrls()
return Boolean(activeListUrls?.includes(url))
return Boolean(DEFAULT_ACTIVE_LIST_URLS?.includes(url))
}
import { createStore, Store } from 'redux'
import { DEFAULT_LIST_OF_LISTS } from '../../constants/lists'
import { DEFAULT_ACTIVE_LIST_URLS } from '../../constants/lists'
import { updateVersion } from '../global/actions'
import { acceptListUpdate, addList, enableList, fetchTokenList, removeList } from './actions'
import { acceptListUpdate, addList, fetchTokenList, removeList } from './actions'
import reducer, { ListsState } from './reducer'
const STUB_TOKEN_LIST = {
......@@ -32,7 +31,6 @@ describe('list reducer', () => {
beforeEach(() => {
store = createStore(reducer, {
byUrl: {},
activeListUrls: undefined,
})
})
......@@ -63,7 +61,6 @@ describe('list reducer', () => {
loadingRequestId: null,
},
},
activeListUrls: undefined,
})
store.dispatch(fetchTokenList.pending({ requestId: 'request-id', url: 'fake-url' }))
......@@ -200,7 +197,6 @@ describe('list reducer', () => {
pendingUpdate: null,
},
},
activeListUrls: undefined,
})
store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' }))
expect(store.getState()).toEqual({
......@@ -243,7 +239,6 @@ describe('list reducer', () => {
pendingUpdate: null,
},
},
activeListUrls: undefined,
})
store.dispatch(addList('fake-url'))
expect(store.getState()).toEqual({
......@@ -271,7 +266,6 @@ describe('list reducer', () => {
pendingUpdate: PATCHED_STUB_LIST,
},
},
activeListUrls: undefined,
})
store.dispatch(acceptListUpdate('fake-url'))
expect(store.getState()).toEqual({
......@@ -299,7 +293,6 @@ describe('list reducer', () => {
pendingUpdate: PATCHED_STUB_LIST,
},
},
activeListUrls: undefined,
})
store.dispatch(removeList('fake-url'))
expect(store.getState()).toEqual({
......@@ -307,110 +300,7 @@ describe('list reducer', () => {
activeListUrls: undefined,
})
})
it('Removes from active lists if active list is removed', () => {
store = createStore(reducer, {
byUrl: {
'fake-url': {
error: null,
current: STUB_TOKEN_LIST,
loadingRequestId: null,
pendingUpdate: PATCHED_STUB_LIST,
},
},
activeListUrls: ['fake-url'],
})
store.dispatch(removeList('fake-url'))
expect(store.getState()).toEqual({
byUrl: {},
activeListUrls: [],
})
})
})
describe('enableList', () => {
it('enables a list url', () => {
store = createStore(reducer, {
byUrl: {
'fake-url': {
error: null,
current: STUB_TOKEN_LIST,
loadingRequestId: null,
pendingUpdate: PATCHED_STUB_LIST,
},
},
activeListUrls: undefined,
})
store.dispatch(enableList('fake-url'))
expect(store.getState()).toEqual({
byUrl: {
'fake-url': {
error: null,
current: STUB_TOKEN_LIST,
loadingRequestId: null,
pendingUpdate: PATCHED_STUB_LIST,
},
},
activeListUrls: ['fake-url'],
})
})
it('adds to url keys if not present already on enable', () => {
store = createStore(reducer, {
byUrl: {
'fake-url': {
error: null,
current: STUB_TOKEN_LIST,
loadingRequestId: null,
pendingUpdate: PATCHED_STUB_LIST,
},
},
activeListUrls: undefined,
})
store.dispatch(enableList('fake-url-invalid'))
expect(store.getState()).toEqual({
byUrl: {
'fake-url': {
error: null,
current: STUB_TOKEN_LIST,
loadingRequestId: null,
pendingUpdate: PATCHED_STUB_LIST,
},
'fake-url-invalid': {
error: null,
current: null,
loadingRequestId: null,
pendingUpdate: null,
},
},
activeListUrls: ['fake-url-invalid'],
})
})
it('enable works if list already added', () => {
store = createStore(reducer, {
byUrl: {
'fake-url': {
error: null,
current: null,
loadingRequestId: null,
pendingUpdate: null,
},
},
activeListUrls: undefined,
})
store.dispatch(enableList('fake-url'))
expect(store.getState()).toEqual({
byUrl: {
'fake-url': {
error: null,
current: null,
loadingRequestId: null,
pendingUpdate: null,
},
},
activeListUrls: ['fake-url'],
})
})
})
describe('updateVersion', () => {
describe('never initialized', () => {
beforeEach(() => {
......@@ -429,7 +319,6 @@ describe('list reducer', () => {
pendingUpdate: null,
},
},
activeListUrls: undefined,
})
store.dispatch(updateVersion())
})
......@@ -458,9 +347,6 @@ describe('list reducer', () => {
it('sets initialized lists', () => {
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS)
})
it('sets selected list', () => {
expect(store.getState().activeListUrls).toEqual(DEFAULT_ACTIVE_LIST_URLS)
})
})
describe('initialized with a different set of lists', () => {
beforeEach(() => {
......@@ -479,7 +365,6 @@ describe('list reducer', () => {
pendingUpdate: null,
},
},
activeListUrls: undefined,
lastInitializedDefaultListOfLists: ['https://unpkg.com/@uniswap/default-token-list@latest'],
})
store.dispatch(updateVersion())
......@@ -518,9 +403,6 @@ describe('list reducer', () => {
it('sets initialized lists', () => {
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS)
})
it('sets default list to selected list', () => {
expect(store.getState().activeListUrls).toEqual(DEFAULT_ACTIVE_LIST_URLS)
})
})
})
})
import { createReducer } from '@reduxjs/toolkit'
import { getVersionUpgrade, TokenList, VersionUpgrade } from '@uniswap/token-lists'
import { DEFAULT_ACTIVE_LIST_URLS } from '../../constants/lists'
import { DEFAULT_LIST_OF_LISTS } from '../../constants/lists'
import { updateVersion } from '../global/actions'
import { acceptListUpdate, addList, disableList, enableList, fetchTokenList, removeList } from './actions'
import { acceptListUpdate, addList, fetchTokenList, removeList } from './actions'
export interface ListsState {
readonly byUrl: {
......@@ -17,9 +16,6 @@ export interface ListsState {
}
// this contains the default list of lists from the last time the updateVersion was called, i.e. the app was reloaded
readonly lastInitializedDefaultListOfLists?: string[]
// currently active lists
readonly activeListUrls: string[] | undefined
}
type ListState = ListsState['byUrl'][string]
......@@ -41,7 +37,6 @@ const initialState: ListsState = {
return memo
}, {}),
},
activeListUrls: DEFAULT_ACTIVE_LIST_URLS,
}
export default createReducer(initialState, (builder) =>
......@@ -75,11 +70,6 @@ export default createReducer(initialState, (builder) =>
}
}
} else {
// activate if on default active
if (DEFAULT_ACTIVE_LIST_URLS.includes(url)) {
state.activeListUrls?.push(url)
}
state.byUrl[url] = {
current: tokenList,
pendingUpdate: null,
......@@ -110,28 +100,6 @@ export default createReducer(initialState, (builder) =>
if (state.byUrl[url]) {
delete state.byUrl[url]
}
// remove list from active urls if needed
if (state.activeListUrls && state.activeListUrls.includes(url)) {
state.activeListUrls = state.activeListUrls.filter((u) => u !== url)
}
})
.addCase(enableList, (state, { payload: url }) => {
if (!state.byUrl[url]) {
state.byUrl[url] = NEW_LIST_STATE
}
if (state.activeListUrls && !state.activeListUrls.includes(url)) {
state.activeListUrls.push(url)
}
if (!state.activeListUrls) {
state.activeListUrls = [url]
}
})
.addCase(disableList, (state, { payload: url }) => {
if (state.activeListUrls && state.activeListUrls.includes(url)) {
state.activeListUrls = state.activeListUrls.filter((u) => u !== url)
}
})
.addCase(acceptListUpdate, (state, { payload: url }) => {
if (!state.byUrl[url]?.pendingUpdate) {
......@@ -147,7 +115,6 @@ export default createReducer(initialState, (builder) =>
// state loaded from localStorage, but new lists have never been initialized
if (!state.lastInitializedDefaultListOfLists) {
state.byUrl = initialState.byUrl
state.activeListUrls = initialState.activeListUrls
} else if (state.lastInitializedDefaultListOfLists) {
const lastInitializedSet = state.lastInitializedDefaultListOfLists.reduce<Set<string>>(
(s, l) => s.add(l),
......@@ -169,18 +136,5 @@ export default createReducer(initialState, (builder) =>
}
state.lastInitializedDefaultListOfLists = DEFAULT_LIST_OF_LISTS
// if no active lists, activate defaults
if (!state.activeListUrls) {
state.activeListUrls = DEFAULT_ACTIVE_LIST_URLS
// for each list on default list, initialize if needed
DEFAULT_ACTIVE_LIST_URLS.map((listUrl: string) => {
if (!state.byUrl[listUrl]) {
state.byUrl[listUrl] = NEW_LIST_STATE
}
return true
})
}
})
)
import { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists'
import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains'
import { ARBITRUM_LIST, CELO_LIST, OPTIMISM_LIST, UNSUPPORTED_LIST_URLS } from 'constants/lists'
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
import useInterval from 'lib/hooks/useInterval'
import { useCallback, useEffect } from 'react'
import { useAppDispatch } from 'state/hooks'
import { useAllLists } from 'state/lists/hooks'
import { isCelo } from '../../constants/tokens'
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
import { acceptListUpdate, enableList } from './actions'
import { useActiveListUrls } from './hooks'
import { acceptListUpdate } from './actions'
export default function Updater(): null {
const { chainId, provider } = useWeb3React()
const { provider } = useWeb3React()
const dispatch = useAppDispatch()
const isWindowVisible = useIsWindowVisible()
// get all loaded lists, and the active urls
const lists = useAllLists()
const activeListUrls = useActiveListUrls()
const fetchList = useFetchListCallback()
const fetchAllListsCallback = useCallback(() => {
......@@ -32,17 +27,6 @@ export default function Updater(): null {
})
}, [fetchList, isWindowVisible, lists])
useEffect(() => {
if (chainId && [SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISM_GOERLI].includes(chainId)) {
dispatch(enableList(OPTIMISM_LIST))
}
if (chainId && [SupportedChainId.ARBITRUM_ONE, SupportedChainId.ARBITRUM_RINKEBY].includes(chainId)) {
dispatch(enableList(ARBITRUM_LIST))
}
if (chainId && isCelo(chainId)) {
dispatch(enableList(CELO_LIST))
}
}, [chainId, dispatch])
// fetch all lists every 10 minutes, but only after we initialize provider
useInterval(fetchAllListsCallback, provider ? 1000 * 60 * 10 : null)
......@@ -94,7 +78,7 @@ export default function Updater(): null {
}
}
})
}, [dispatch, lists, activeListUrls])
}, [dispatch, lists])
return null
}
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