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 { useWeb3React } from '@web3-react/core'
import clsx from 'clsx' import clsx from 'clsx'
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow' 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 { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety'
import { getTokenDetailsURL } from 'graphql/data/util' import { getTokenDetailsURL } from 'graphql/data/util'
import uriToHttp from 'lib/utils/uriToHttp' import uriToHttp from 'lib/utils/uriToHttp'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
...@@ -87,7 +88,6 @@ export const CollectionRow = ({ ...@@ -87,7 +88,6 @@ export const CollectionRow = ({
<Column className={styles.suggestionPrimaryContainer}> <Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full"> <Row gap="4" width="full">
<Box className={styles.primaryText}>{collection.name}</Box> <Box className={styles.primaryText}>{collection.name}</Box>
{collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />}
</Row> </Row>
<Box className={styles.secondaryText}>{putCommas(collection.stats.total_supply)} items</Box> <Box className={styles.secondaryText}>{putCommas(collection.stats.total_supply)} items</Box>
</Column> </Column>
...@@ -181,7 +181,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, traceE ...@@ -181,7 +181,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, traceE
<Column className={styles.suggestionPrimaryContainer}> <Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full"> <Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box> <Box className={styles.primaryText}>{token.name}</Box>
{token.onDefaultList && <VerifiedIcon className={styles.suggestionIcon} />} <TokenSafetyIcon warning={checkWarning(token.address)} />
</Row> </Row>
<Box className={styles.secondaryText}>{token.symbol}</Box> <Box className={styles.secondaryText}>{token.symbol}</Box>
</Column> </Column>
......
...@@ -2,7 +2,25 @@ ...@@ -2,7 +2,25 @@
exports[`renders currency rows correctly when currencies list is non-empty 1`] = ` exports[`renders currency rows correctly when currencies list is non-empty 1`] = `
<DocumentFragment> <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; color: #99A1BD;
} }
...@@ -55,7 +73,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] = ...@@ -55,7 +73,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
justify-content: space-between; justify-content: space-between;
} }
.c8 { .c10 {
width: -webkit-fit-content; width: -webkit-fit-content;
width: -moz-fit-content; width: -moz-fit-content;
width: fit-content; width: fit-content;
...@@ -111,9 +129,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] = ...@@ -111,9 +129,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
> >
Dai Stablecoin Dai Stablecoin
</div> </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>
<div <div
class="c7 css-1j6a53a" class="c9 css-1j6a53a"
> >
DAI DAI
</div> </div>
...@@ -122,7 +172,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] = ...@@ -122,7 +172,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
class="c4" class="c4"
> >
<div <div
class="c0 c1 c8" class="c0 c1 c10"
style="justify-self: flex-end;" style="justify-self: flex-end;"
/> />
</div> </div>
...@@ -150,9 +200,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] = ...@@ -150,9 +200,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
> >
USD//C USD//C
</div> </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>
<div <div
class="c7 css-1j6a53a" class="c9 css-1j6a53a"
> >
USDC USDC
</div> </div>
...@@ -161,7 +243,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] = ...@@ -161,7 +243,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
class="c4" class="c4"
> >
<div <div
class="c0 c1 c8" class="c0 c1 c10"
style="justify-self: flex-end;" style="justify-self: flex-end;"
/> />
</div> </div>
...@@ -189,9 +271,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] = ...@@ -189,9 +271,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
> >
Wrapped BTC Wrapped BTC
</div> </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>
<div <div
class="c7 css-1j6a53a" class="c9 css-1j6a53a"
> >
WBTC WBTC
</div> </div>
...@@ -200,7 +314,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] = ...@@ -200,7 +314,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
class="c4" class="c4"
> >
<div <div
class="c0 c1 c8" class="c0 c1 c10"
style="justify-self: flex-end;" style="justify-self: flex-end;"
/> />
</div> </div>
......
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants' import { ElementName, Event, EventName } from 'analytics/constants'
...@@ -15,10 +14,8 @@ import styled from 'styled-components/macro' ...@@ -15,10 +14,8 @@ import styled from 'styled-components/macro'
import { useIsUserAddedToken } from '../../../hooks/Tokens' import { useIsUserAddedToken } from '../../../hooks/Tokens'
import { useCurrencyBalance } from '../../../state/connection/hooks' import { useCurrencyBalance } from '../../../state/connection/hooks'
import { useCombinedActiveList } from '../../../state/lists/hooks'
import { WrappedTokenInfo } from '../../../state/lists/wrappedTokenInfo' import { WrappedTokenInfo } from '../../../state/lists/wrappedTokenInfo'
import { ThemedText } from '../../../theme' import { ThemedText } from '../../../theme'
import { isTokenOnList } from '../../../utils'
import Column, { AutoColumn } from '../../Column' import Column, { AutoColumn } from '../../Column'
import CurrencyLogo from '../../CurrencyLogo' import CurrencyLogo from '../../CurrencyLogo'
import Loader from '../../Loader' import Loader from '../../Loader'
...@@ -128,8 +125,6 @@ export function CurrencyRow({ ...@@ -128,8 +125,6 @@ export function CurrencyRow({
}) { }) {
const { account } = useWeb3React() const { account } = useWeb3React()
const key = currencyKey(currency) const key = currencyKey(currency)
const selectedTokenList = useCombinedActiveList()
const isOnSelectedList = isTokenOnList(selectedTokenList, currency.isToken ? currency : undefined)
const customAdded = useIsUserAddedToken(currency) const customAdded = useIsUserAddedToken(currency)
const balance = useCurrencyBalance(account ?? undefined, currency) const balance = useCurrencyBalance(account ?? undefined, currency)
const warning = currency.isNative ? null : checkWarning(currency.address) const warning = currency.isNative ? null : checkWarning(currency.address)
...@@ -170,11 +165,7 @@ export function CurrencyRow({ ...@@ -170,11 +165,7 @@ export function CurrencyRow({
{isBlockedToken && <BlockedTokenIcon />} {isBlockedToken && <BlockedTokenIcon />}
</Row> </Row>
<ThemedText.DeprecatedDarkGray ml="0px" fontSize={'12px'} fontWeight={300}> <ThemedText.DeprecatedDarkGray ml="0px" fontSize={'12px'} fontWeight={300}>
{!currency.isNative && !isOnSelectedList && customAdded ? ( {currency.symbol}
<Trans>{currency.symbol} • Added by user</Trans>
) : (
currency.symbol
)}
</ThemedText.DeprecatedDarkGray> </ThemedText.DeprecatedDarkGray>
</AutoColumn> </AutoColumn>
<Column> <Column>
......
...@@ -19,7 +19,7 @@ import { Text } from 'rebass' ...@@ -19,7 +19,7 @@ import { Text } from 'rebass'
import { useAllTokenBalances } from 'state/connection/hooks' import { useAllTokenBalances } from 'state/connection/hooks'
import styled, { useTheme } from 'styled-components/macro' 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 { CloseIcon, ThemedText } from '../../theme'
import { isAddress } from '../../utils' import { isAddress } from '../../utils'
import Column from '../Column' import Column from '../Column'
...@@ -71,7 +71,7 @@ export function CurrencySearch({ ...@@ -71,7 +71,7 @@ export function CurrencySearch({
const [searchQuery, setSearchQuery] = useState<string>('') const [searchQuery, setSearchQuery] = useState<string>('')
const debouncedQuery = useDebounce(searchQuery, 200) const debouncedQuery = useDebounce(searchQuery, 200)
const allTokens = useAllTokens() const allTokens = useActiveTokens()
// if they input an address, use it // if they input an address, use it
const isAddressSearch = isAddress(debouncedQuery) const isAddressSearch = isAddress(debouncedQuery)
......
import { Currency, Token } from '@uniswap/sdk-core' import { Currency, Token } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists'
import TokenSafety from 'components/TokenSafety' import TokenSafety from 'components/TokenSafety'
import usePrevious from 'hooks/usePrevious'
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { useUserAddedTokens } from 'state/user/hooks' import { useUserAddedTokens } from 'state/user/hooks'
import useLast from '../../hooks/useLast' import useLast from '../../hooks/useLast'
import { useWindowSize } from '../../hooks/useWindowSize' import { useWindowSize } from '../../hooks/useWindowSize'
import Modal from '../Modal' import Modal from '../Modal'
import { CurrencySearch } from './CurrencySearch' import { CurrencySearch } from './CurrencySearch'
import { ImportList } from './ImportList'
import { ImportToken } from './ImportToken'
import Manage from './Manage'
interface CurrencySearchModalProps { interface CurrencySearchModalProps {
isOpen: boolean isOpen: boolean
...@@ -27,9 +21,7 @@ interface CurrencySearchModalProps { ...@@ -27,9 +21,7 @@ interface CurrencySearchModalProps {
export enum CurrencyModalView { export enum CurrencyModalView {
search, search,
manage,
importToken, importToken,
importList,
tokenSafety, tokenSafety,
} }
...@@ -69,25 +61,9 @@ export default memo(function CurrencySearchModal({ ...@@ -69,25 +61,9 @@ export default memo(function CurrencySearchModal({
}, },
[onDismiss, onCurrencySelect, userAddedTokens] [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 // used for token safety
const [warningToken, setWarningToken] = useState<Token | undefined>() const [warningToken, setWarningToken] = useState<Token | undefined>()
const handleBackImport = useCallback(
() => setModalView(prevView && prevView !== CurrencyModalView.importToken ? prevView : CurrencyModalView.search),
[setModalView, prevView]
)
const { height: windowHeight } = useWindowSize() const { height: windowHeight } = useWindowSize()
// change min height if not searching // change min height if not searching
let modalHeight: number | undefined = 80 let modalHeight: number | undefined = 80
...@@ -124,38 +100,6 @@ export default memo(function CurrencySearchModal({ ...@@ -124,38 +100,6 @@ export default memo(function CurrencySearchModal({
) )
} }
break 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 ( return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={modalHeight} minHeight={modalHeight}> <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, WARNING_LEVEL } from 'constants/tokenSafety'
import { Warning } from 'constants/tokenSafety' import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
const VerifiedContainer = styled.div` const VerifiedContainer = styled.div`
...@@ -8,17 +8,17 @@ const VerifiedContainer = styled.div` ...@@ -8,17 +8,17 @@ const VerifiedContainer = styled.div`
justify-content: center; justify-content: center;
` `
export const VerifiedIcon = styled(Verified)<{ size?: string }>` export const WarningIcon = styled(AlertTriangle)<{ size?: string }>`
width: ${({ size }) => size ?? '1em'}; width: ${({ size }) => size ?? '1em'};
height: ${({ size }) => size ?? '1em'}; height: ${({ size }) => size ?? '1em'};
color: ${({ theme }) => theme.accentAction}; color: ${({ theme }) => theme.textTertiary};
` `
export default function TokenSafetyIcon({ warning }: { warning: Warning | null }) { export default function TokenSafetyIcon({ warning }: { warning: Warning | null }) {
if (warning) return null if (warning?.level !== WARNING_LEVEL.UNKNOWN) return null
return ( return (
<VerifiedContainer> <VerifiedContainer>
<VerifiedIcon /> <WarningIcon />
</VerifiedContainer> </VerifiedContainer>
) )
} }
...@@ -9,7 +9,7 @@ import { Color } from 'theme/styled' ...@@ -9,7 +9,7 @@ import { Color } from 'theme/styled'
const Label = styled.div<{ color: Color }>` const Label = styled.div<{ color: Color }>`
width: 100%; width: 100%;
padding: 12px 20px; padding: 12px 20px 16px;
background-color: ${({ color }) => color + '1F'}; background-color: ${({ color }) => color + '1F'};
border-radius: 16px; border-radius: 16px;
color: ${({ color }) => color}; color: ${({ color }) => color};
...@@ -31,9 +31,15 @@ const Title = styled(Text)` ...@@ -31,9 +31,15 @@ const Title = styled(Text)`
const DetailsRow = styled.div` const DetailsRow = styled.div`
margin-top: 8px; margin-top: 8px;
font-size: 12px; font-size: 12px;
line-height: 16px;
color: ${({ theme }) => theme.textSecondary}; color: ${({ theme }) => theme.textSecondary};
` `
const StyledLink = styled(ExternalLink)`
color: ${({ theme }) => theme.textSecondary};
font-weight: 700;
`
type TokenWarningMessageProps = { type TokenWarningMessageProps = {
warning: Warning warning: Warning
tokenAddress: string tokenAddress: string
...@@ -56,9 +62,9 @@ export default function TokenWarningMessage({ warning, tokenAddress }: TokenWarn ...@@ -56,9 +62,9 @@ export default function TokenWarningMessage({ warning, tokenAddress }: TokenWarn
{description} {description}
{Boolean(description) && ' '} {Boolean(description) && ' '}
{tokenAddress && ( {tokenAddress && (
<ExternalLink href={TOKEN_SAFETY_ARTICLE}> <StyledLink href={TOKEN_SAFETY_ARTICLE}>
<Trans>Learn more</Trans> <Trans>Learn more</Trans>
</ExternalLink> </StyledLink>
)} )}
</DetailsRow> </DetailsRow>
</Label> </Label>
......
...@@ -244,6 +244,11 @@ export default function TokenSafety({ ...@@ -244,6 +244,11 @@ export default function TokenSafety({
} }
const { heading, description } = getWarningCopy(displayWarning, plural) const { heading, description } = getWarningCopy(displayWarning, plural)
const learnMoreUrl = (
<StyledExternalLink href={TOKEN_SAFETY_ARTICLE}>
<Trans>Learn more</Trans>
</StyledExternalLink>
)
return ( return (
displayWarning && ( displayWarning && (
...@@ -255,13 +260,9 @@ export default function TokenSafety({ ...@@ -255,13 +260,9 @@ export default function TokenSafety({
<ShortColumn> <ShortColumn>
<SafetyLabel warning={displayWarning} /> <SafetyLabel warning={displayWarning} />
</ShortColumn> </ShortColumn>
<ShortColumn>{heading && <InfoText fontSize="20px">{heading}</InfoText>}</ShortColumn>
<ShortColumn> <ShortColumn>
<InfoText> <InfoText>
{description}{' '} {heading} {description} {learnMoreUrl}
<StyledExternalLink href={TOKEN_SAFETY_ARTICLE}>
<Trans>Learn more</Trans>
</StyledExternalLink>
</InfoText> </InfoText>
</ShortColumn> </ShortColumn>
<LinkColumn>{urls}</LinkColumn> <LinkColumn>{urls}</LinkColumn>
......
...@@ -2,9 +2,7 @@ import { Trans } from '@lingui/macro' ...@@ -2,9 +2,7 @@ import { Trans } from '@lingui/macro'
import { Currency, NativeCurrency, Token } from '@uniswap/sdk-core' import { Currency, NativeCurrency, Token } from '@uniswap/sdk-core'
import { ParentSize } from '@visx/responsive' import { ParentSize } from '@visx/responsive'
import CurrencyLogo from 'components/CurrencyLogo' import CurrencyLogo from 'components/CurrencyLogo'
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety'
import { PriceDurations, PricePoint, SingleTokenData } from 'graphql/data/Token' import { PriceDurations, PricePoint, SingleTokenData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens' import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod } from 'graphql/data/util' import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod } from 'graphql/data/util'
...@@ -80,7 +78,6 @@ export default function ChartSection({ ...@@ -80,7 +78,6 @@ export default function ChartSection({
}) { }) {
const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain] const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
const L2Icon = getChainInfo(chainId)?.circleLogoUrl const L2Icon = getChainInfo(chainId)?.circleLogoUrl
const warning = checkWarning(token.address ?? '')
const timePeriod = useAtomValue(filterTimeAtom) const timePeriod = useAtomValue(filterTimeAtom)
const logoSrc = useTokenLogoURI(token, nativeCurrency) const logoSrc = useTokenLogoURI(token, nativeCurrency)
...@@ -120,7 +117,6 @@ export default function ChartSection({ ...@@ -120,7 +117,6 @@ export default function ChartSection({
</LogoContainer> </LogoContainer>
{nativeCurrency?.name ?? token.name ?? <Trans>Name not found</Trans>} {nativeCurrency?.name ?? token.name ?? <Trans>Name not found</Trans>}
<TokenSymbol>{nativeCurrency?.symbol ?? token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol> <TokenSymbol>{nativeCurrency?.symbol ?? token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
{!warning && <VerifiedIcon size="16px" />}
</TokenNameCell> </TokenNameCell>
<TokenActions> <TokenActions>
{token.name && token.symbol && token.address && <ShareButton token={token} isNative={!!nativeCurrency} />} {token.name && token.symbol && token.address && <ShareButton token={token} isNative={!!nativeCurrency} />}
......
export const UNI_LIST = 'https://tokens.uniswap.org' export const UNI_LIST = 'https://tokens.uniswap.org'
export const UNI_EXTENDED_LIST = 'https://extendedtokens.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 AAVE_LIST = 'tokenlist.aave.eth'
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json' 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' 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 ...@@ -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 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 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 // default lists to be 'active' aka searched across
// lower index == higher priority for token import export const DEFAULT_ACTIVE_LIST_URLS: string[] = [UNI_LIST]
const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [ export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
UNI_LIST,
UNI_EXTENDED_LIST, UNI_EXTENDED_LIST,
COMPOUND_LIST, COMPOUND_LIST,
AAVE_LIST, AAVE_LIST,
...@@ -37,10 +36,11 @@ const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [ ...@@ -37,10 +36,11 @@ const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [
CELO_LIST, 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[] = [ export const DEFAULT_LIST_OF_LISTS: string[] = [
...DEFAULT_LIST_OF_LISTS_TO_DISPLAY, ...DEFAULT_LIST_OF_LISTS_TO_DISPLAY,
...UNSUPPORTED_LIST_URLS, // need to load dynamic unsupported tokens as well ...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 { Plural, Trans } from '@lingui/macro'
import { ZERO_ADDRESS } from './misc'
import { NATIVE_CHAIN_ID } from './tokens'
import WarningCache, { TOKEN_LIST_TYPES } from './TokenSafetyLookupTable' import WarningCache, { TOKEN_LIST_TYPES } from './TokenSafetyLookupTable'
export const TOKEN_SAFETY_ARTICLE = 'https://support.uniswap.org/hc/en-us/articles/8723118437133' export const TOKEN_SAFETY_ARTICLE = 'https://support.uniswap.org/hc/en-us/articles/8723118437133'
...@@ -14,17 +16,36 @@ export function getWarningCopy(warning: Warning | null, plural = false) { ...@@ -14,17 +16,36 @@ export function getWarningCopy(warning: Warning | null, plural = false) {
let heading = null, let heading = null,
description = null description = null
if (warning) { if (warning) {
if (warning.canProceed) { switch (warning.level) {
heading = <Plural value={plural ? 2 : 1} _1="This token isn't verified." other="These tokens aren't verified." /> case WARNING_LEVEL.MEDIUM:
description = <Trans>Please do your own research before trading.</Trans> heading = (
} else { <Plural
description = ( value={plural ? 2 : 1}
<Plural _1="This token isn't traded on leading U.S. centralized exchanges."
value={plural ? 2 : 1} other="These tokens aren't traded on leading U.S. centralized exchanges."
_1="You can't trade this token using the Uniswap App." />
other="You can't trade these tokens using the Uniswap App." )
/> 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}
_1="You can't trade this token using the Uniswap App."
other="You can't trade these tokens using the Uniswap App."
/>
)
break
} }
} }
return { heading, description } return { heading, description }
...@@ -57,6 +78,9 @@ const BlockedWarning: Warning = { ...@@ -57,6 +78,9 @@ const BlockedWarning: Warning = {
} }
export function checkWarning(tokenAddress: string) { export function checkWarning(tokenAddress: string) {
if (tokenAddress === NATIVE_CHAIN_ID || tokenAddress === ZERO_ADDRESS) {
return null
}
switch (WarningCache.checkToken(tokenAddress.toLowerCase())) { switch (WarningCache.checkToken(tokenAddress.toLowerCase())) {
case TOKEN_LIST_TYPES.UNI_DEFAULT: case TOKEN_LIST_TYPES.UNI_DEFAULT:
return null return null
......
...@@ -2,12 +2,13 @@ import { Currency, Token } from '@uniswap/sdk-core' ...@@ -2,12 +2,13 @@ import { Currency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { DEFAULT_INACTIVE_LIST_URLS } from 'constants/lists'
import { useCurrencyFromMap, useTokenFromMapOrNetwork } from 'lib/hooks/useCurrency' import { useCurrencyFromMap, useTokenFromMapOrNetwork } from 'lib/hooks/useCurrency'
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering' import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
import { useMemo } from 'react' import { useMemo } from 'react'
import { isL2ChainId } from 'utils/chains' 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 { WrappedTokenInfo } from '../state/lists/wrappedTokenInfo'
import { useUserAddedTokens, useUserAddedTokensOnChain } from '../state/user/hooks' import { useUserAddedTokens, useUserAddedTokensOnChain } from '../state/user/hooks'
import { TokenAddressMap, useUnsupportedTokenList } from './../state/lists/hooks' import { TokenAddressMap, useUnsupportedTokenList } from './../state/lists/hooks'
...@@ -54,6 +55,11 @@ export function useAllTokens(): { [address: string]: Token } { ...@@ -54,6 +55,11 @@ export function useAllTokens(): { [address: string]: Token } {
return useTokensFromMap(allTokens, true) return useTokensFromMap(allTokens, true)
} }
export function useActiveTokens(): { [address: string]: Token } {
const allTokens = useCombinedActiveList()
return useTokensFromMap(allTokens, false)
}
type BridgeInfo = Record< type BridgeInfo = Record<
SupportedChainId, SupportedChainId,
{ {
...@@ -109,7 +115,7 @@ export function useUnsupportedTokens(): { [address: string]: Token } { ...@@ -109,7 +115,7 @@ export function useUnsupportedTokens(): { [address: string]: Token } {
export function useSearchInactiveTokenLists(search: string | undefined, minResults = 10): WrappedTokenInfo[] { export function useSearchInactiveTokenLists(search: string | undefined, minResults = 10): WrappedTokenInfo[] {
const lists = useAllLists() const lists = useAllLists()
const inactiveUrls = useInactiveListUrls() const inactiveUrls = DEFAULT_INACTIVE_LIST_URLS
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const activeTokens = useAllTokens() const activeTokens = useAllTokens()
return useMemo(() => { return useMemo(() => {
......
...@@ -16,7 +16,7 @@ function balanceComparator(a?: CurrencyAmount<Currency>, b?: CurrencyAmount<Curr ...@@ -16,7 +16,7 @@ function balanceComparator(a?: CurrencyAmount<Currency>, b?: CurrencyAmount<Curr
type TokenBalances = { [tokenAddress: string]: CurrencyAmount<Token> | undefined } 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) { export function tokenComparator(balances: TokenBalances, a: Token, b: Token) {
// Sorts by balances // Sorts by balances
const balanceComparison = balanceComparator(balances[a.address], balances[b.address]) const balanceComparison = balanceComparator(balances[a.address], balances[b.address])
......
...@@ -14,9 +14,5 @@ export const fetchTokenList: Readonly<{ ...@@ -14,9 +14,5 @@ export const fetchTokenList: Readonly<{
export const addList = createAction<string>('lists/addList') export const addList = createAction<string>('lists/addList')
export const removeList = createAction<string>('lists/removeList') 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 // versioning
export const acceptListUpdate = createAction<string>('lists/acceptListUpdate') export const acceptListUpdate = createAction<string>('lists/acceptListUpdate')
...@@ -5,7 +5,7 @@ import sortByListPriority from 'utils/listSort' ...@@ -5,7 +5,7 @@ import sortByListPriority from 'utils/listSort'
import BROKEN_LIST from '../../constants/tokenLists/broken.tokenlist.json' import BROKEN_LIST from '../../constants/tokenLists/broken.tokenlist.json'
import { AppState } from '../index' 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 export type TokenAddressMap = ChainTokenMap
...@@ -66,25 +66,9 @@ function useCombinedTokenMapFromUrls(urls: string[] | undefined): TokenAddressMa ...@@ -66,25 +66,9 @@ function useCombinedTokenMapFromUrls(urls: string[] | undefined): TokenAddressMa
}, [lists, urls]) }, [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 // get all the tokens from active lists, combine with local default tokens
export function useCombinedActiveList(): TokenAddressMap { export function useCombinedActiveList(): TokenAddressMap {
const activeListUrls = useActiveListUrls() const activeTokens = useCombinedTokenMapFromUrls(DEFAULT_ACTIVE_LIST_URLS)
const activeTokens = useCombinedTokenMapFromUrls(activeListUrls)
return activeTokens return activeTokens
} }
...@@ -100,6 +84,5 @@ export function useUnsupportedTokenList(): TokenAddressMap { ...@@ -100,6 +84,5 @@ export function useUnsupportedTokenList(): TokenAddressMap {
return useMemo(() => combineMaps(brokenListMap, loadedUnsupportedListMap), [brokenListMap, loadedUnsupportedListMap]) return useMemo(() => combineMaps(brokenListMap, loadedUnsupportedListMap), [brokenListMap, loadedUnsupportedListMap])
} }
export function useIsListActive(url: string): boolean { export function useIsListActive(url: string): boolean {
const activeListUrls = useActiveListUrls() return Boolean(DEFAULT_ACTIVE_LIST_URLS?.includes(url))
return Boolean(activeListUrls?.includes(url))
} }
import { createStore, Store } from 'redux' import { createStore, Store } from 'redux'
import { DEFAULT_LIST_OF_LISTS } from '../../constants/lists' import { DEFAULT_LIST_OF_LISTS } from '../../constants/lists'
import { DEFAULT_ACTIVE_LIST_URLS } from '../../constants/lists'
import { updateVersion } from '../global/actions' 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' import reducer, { ListsState } from './reducer'
const STUB_TOKEN_LIST = { const STUB_TOKEN_LIST = {
...@@ -32,7 +31,6 @@ describe('list reducer', () => { ...@@ -32,7 +31,6 @@ describe('list reducer', () => {
beforeEach(() => { beforeEach(() => {
store = createStore(reducer, { store = createStore(reducer, {
byUrl: {}, byUrl: {},
activeListUrls: undefined,
}) })
}) })
...@@ -63,7 +61,6 @@ describe('list reducer', () => { ...@@ -63,7 +61,6 @@ describe('list reducer', () => {
loadingRequestId: null, loadingRequestId: null,
}, },
}, },
activeListUrls: undefined,
}) })
store.dispatch(fetchTokenList.pending({ requestId: 'request-id', url: 'fake-url' })) store.dispatch(fetchTokenList.pending({ requestId: 'request-id', url: 'fake-url' }))
...@@ -200,7 +197,6 @@ describe('list reducer', () => { ...@@ -200,7 +197,6 @@ describe('list reducer', () => {
pendingUpdate: null, pendingUpdate: null,
}, },
}, },
activeListUrls: undefined,
}) })
store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' })) store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' }))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
...@@ -243,7 +239,6 @@ describe('list reducer', () => { ...@@ -243,7 +239,6 @@ describe('list reducer', () => {
pendingUpdate: null, pendingUpdate: null,
}, },
}, },
activeListUrls: undefined,
}) })
store.dispatch(addList('fake-url')) store.dispatch(addList('fake-url'))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
...@@ -271,7 +266,6 @@ describe('list reducer', () => { ...@@ -271,7 +266,6 @@ describe('list reducer', () => {
pendingUpdate: PATCHED_STUB_LIST, pendingUpdate: PATCHED_STUB_LIST,
}, },
}, },
activeListUrls: undefined,
}) })
store.dispatch(acceptListUpdate('fake-url')) store.dispatch(acceptListUpdate('fake-url'))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
...@@ -299,7 +293,6 @@ describe('list reducer', () => { ...@@ -299,7 +293,6 @@ describe('list reducer', () => {
pendingUpdate: PATCHED_STUB_LIST, pendingUpdate: PATCHED_STUB_LIST,
}, },
}, },
activeListUrls: undefined,
}) })
store.dispatch(removeList('fake-url')) store.dispatch(removeList('fake-url'))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
...@@ -307,110 +300,7 @@ describe('list reducer', () => { ...@@ -307,110 +300,7 @@ describe('list reducer', () => {
activeListUrls: undefined, 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('updateVersion', () => {
describe('never initialized', () => { describe('never initialized', () => {
beforeEach(() => { beforeEach(() => {
...@@ -429,7 +319,6 @@ describe('list reducer', () => { ...@@ -429,7 +319,6 @@ describe('list reducer', () => {
pendingUpdate: null, pendingUpdate: null,
}, },
}, },
activeListUrls: undefined,
}) })
store.dispatch(updateVersion()) store.dispatch(updateVersion())
}) })
...@@ -458,9 +347,6 @@ describe('list reducer', () => { ...@@ -458,9 +347,6 @@ describe('list reducer', () => {
it('sets initialized lists', () => { it('sets initialized lists', () => {
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_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', () => { describe('initialized with a different set of lists', () => {
beforeEach(() => { beforeEach(() => {
...@@ -479,7 +365,6 @@ describe('list reducer', () => { ...@@ -479,7 +365,6 @@ describe('list reducer', () => {
pendingUpdate: null, pendingUpdate: null,
}, },
}, },
activeListUrls: undefined,
lastInitializedDefaultListOfLists: ['https://unpkg.com/@uniswap/default-token-list@latest'], lastInitializedDefaultListOfLists: ['https://unpkg.com/@uniswap/default-token-list@latest'],
}) })
store.dispatch(updateVersion()) store.dispatch(updateVersion())
...@@ -518,9 +403,6 @@ describe('list reducer', () => { ...@@ -518,9 +403,6 @@ describe('list reducer', () => {
it('sets initialized lists', () => { it('sets initialized lists', () => {
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_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 { createReducer } from '@reduxjs/toolkit'
import { getVersionUpgrade, TokenList, VersionUpgrade } from '@uniswap/token-lists' 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 { DEFAULT_LIST_OF_LISTS } from '../../constants/lists'
import { updateVersion } from '../global/actions' import { updateVersion } from '../global/actions'
import { acceptListUpdate, addList, disableList, enableList, fetchTokenList, removeList } from './actions' import { acceptListUpdate, addList, fetchTokenList, removeList } from './actions'
export interface ListsState { export interface ListsState {
readonly byUrl: { readonly byUrl: {
...@@ -17,9 +16,6 @@ export interface ListsState { ...@@ -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 // this contains the default list of lists from the last time the updateVersion was called, i.e. the app was reloaded
readonly lastInitializedDefaultListOfLists?: string[] readonly lastInitializedDefaultListOfLists?: string[]
// currently active lists
readonly activeListUrls: string[] | undefined
} }
type ListState = ListsState['byUrl'][string] type ListState = ListsState['byUrl'][string]
...@@ -41,7 +37,6 @@ const initialState: ListsState = { ...@@ -41,7 +37,6 @@ const initialState: ListsState = {
return memo return memo
}, {}), }, {}),
}, },
activeListUrls: DEFAULT_ACTIVE_LIST_URLS,
} }
export default createReducer(initialState, (builder) => export default createReducer(initialState, (builder) =>
...@@ -75,11 +70,6 @@ export default createReducer(initialState, (builder) => ...@@ -75,11 +70,6 @@ export default createReducer(initialState, (builder) =>
} }
} }
} else { } else {
// activate if on default active
if (DEFAULT_ACTIVE_LIST_URLS.includes(url)) {
state.activeListUrls?.push(url)
}
state.byUrl[url] = { state.byUrl[url] = {
current: tokenList, current: tokenList,
pendingUpdate: null, pendingUpdate: null,
...@@ -110,28 +100,6 @@ export default createReducer(initialState, (builder) => ...@@ -110,28 +100,6 @@ export default createReducer(initialState, (builder) =>
if (state.byUrl[url]) { if (state.byUrl[url]) {
delete 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 }) => { .addCase(acceptListUpdate, (state, { payload: url }) => {
if (!state.byUrl[url]?.pendingUpdate) { if (!state.byUrl[url]?.pendingUpdate) {
...@@ -147,7 +115,6 @@ export default createReducer(initialState, (builder) => ...@@ -147,7 +115,6 @@ export default createReducer(initialState, (builder) =>
// state loaded from localStorage, but new lists have never been initialized // state loaded from localStorage, but new lists have never been initialized
if (!state.lastInitializedDefaultListOfLists) { if (!state.lastInitializedDefaultListOfLists) {
state.byUrl = initialState.byUrl state.byUrl = initialState.byUrl
state.activeListUrls = initialState.activeListUrls
} else if (state.lastInitializedDefaultListOfLists) { } else if (state.lastInitializedDefaultListOfLists) {
const lastInitializedSet = state.lastInitializedDefaultListOfLists.reduce<Set<string>>( const lastInitializedSet = state.lastInitializedDefaultListOfLists.reduce<Set<string>>(
(s, l) => s.add(l), (s, l) => s.add(l),
...@@ -169,18 +136,5 @@ export default createReducer(initialState, (builder) => ...@@ -169,18 +136,5 @@ export default createReducer(initialState, (builder) =>
} }
state.lastInitializedDefaultListOfLists = DEFAULT_LIST_OF_LISTS 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 { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains' import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
import { ARBITRUM_LIST, CELO_LIST, OPTIMISM_LIST, UNSUPPORTED_LIST_URLS } from 'constants/lists'
import useInterval from 'lib/hooks/useInterval' import useInterval from 'lib/hooks/useInterval'
import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import { useAppDispatch } from 'state/hooks' import { useAppDispatch } from 'state/hooks'
import { useAllLists } from 'state/lists/hooks' import { useAllLists } from 'state/lists/hooks'
import { isCelo } from '../../constants/tokens'
import { useFetchListCallback } from '../../hooks/useFetchListCallback' import { useFetchListCallback } from '../../hooks/useFetchListCallback'
import useIsWindowVisible from '../../hooks/useIsWindowVisible' import useIsWindowVisible from '../../hooks/useIsWindowVisible'
import { acceptListUpdate, enableList } from './actions' import { acceptListUpdate } from './actions'
import { useActiveListUrls } from './hooks'
export default function Updater(): null { export default function Updater(): null {
const { chainId, provider } = useWeb3React() const { provider } = useWeb3React()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const isWindowVisible = useIsWindowVisible() const isWindowVisible = useIsWindowVisible()
// get all loaded lists, and the active urls // get all loaded lists, and the active urls
const lists = useAllLists() const lists = useAllLists()
const activeListUrls = useActiveListUrls()
const fetchList = useFetchListCallback() const fetchList = useFetchListCallback()
const fetchAllListsCallback = useCallback(() => { const fetchAllListsCallback = useCallback(() => {
...@@ -32,17 +27,6 @@ export default function Updater(): null { ...@@ -32,17 +27,6 @@ export default function Updater(): null {
}) })
}, [fetchList, isWindowVisible, lists]) }, [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 // fetch all lists every 10 minutes, but only after we initialize provider
useInterval(fetchAllListsCallback, provider ? 1000 * 60 * 10 : null) useInterval(fetchAllListsCallback, provider ? 1000 * 60 * 10 : null)
...@@ -94,7 +78,7 @@ export default function Updater(): null { ...@@ -94,7 +78,7 @@ export default function Updater(): null {
} }
} }
}) })
}, [dispatch, lists, activeListUrls]) }, [dispatch, lists])
return null 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