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
This commit is contained in:
parent
a920a93b3d
commit
d9434a1a9c
@ -1,8 +1,9 @@
|
||||
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>
|
||||
|
@ -1,4 +1,3 @@
|
||||
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)
|
||||
|
@ -1,18 +1,12 @@
|
||||
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}>
|
||||
|
@ -1,167 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
@ -1,415 +0,0 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import Card from 'components/Card'
|
||||
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
|
||||
import { useListColor } from 'hooks/useColor'
|
||||
import parseENSAddress from 'lib/utils/parseENSAddress'
|
||||
import uriToHttp from 'lib/utils/uriToHttp'
|
||||
import { ChangeEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { CheckCircle, Settings } from 'react-feather'
|
||||
import { usePopper } from 'react-popper'
|
||||
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
import { acceptListUpdate, disableList, enableList, removeList } from '../../state/lists/actions'
|
||||
import { useActiveListUrls, useAllLists, useIsListActive } from '../../state/lists/hooks'
|
||||
import { ExternalLink, IconWrapper, LinkStyledButton, ThemedText } from '../../theme'
|
||||
import listVersionLabel from '../../utils/listVersionLabel'
|
||||
import { ButtonEmpty, ButtonPrimary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import Row, { RowBetween, RowFixed } from '../Row'
|
||||
import Toggle from '../Toggle'
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
|
||||
|
||||
const Wrapper = styled(Column)`
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
`
|
||||
|
||||
const UnpaddedLinkStyledButton = styled(LinkStyledButton)`
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 100;
|
||||
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${(props) => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
background: ${({ theme }) => theme.deprecated_bg2};
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_bg3};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
color: ${({ theme }) => theme.deprecated_text2};
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 8px;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
`
|
||||
|
||||
const StyledTitleText = styled.div<{ active: boolean }>`
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
color: ${({ theme, active }) => (active ? theme.deprecated_white : theme.deprecated_text2)};
|
||||
`
|
||||
|
||||
const StyledListUrlText = styled(ThemedText.DeprecatedMain)<{ active: boolean }>`
|
||||
font-size: 12px;
|
||||
color: ${({ theme, active }) => (active ? theme.deprecated_white : theme.deprecated_text2)};
|
||||
`
|
||||
|
||||
const RowWrapper = styled(Row)<{ bgColor: string; active: boolean; hasActiveTokens: boolean }>`
|
||||
background-color: ${({ bgColor, active, theme }) => (active ? bgColor ?? 'transparent' : theme.deprecated_bg2)};
|
||||
opacity: ${({ hasActiveTokens }) => (hasActiveTokens ? 1 : 0.4)};
|
||||
transition: 200ms;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
function listUrlRowHTMLId(listUrl: string) {
|
||||
return `list-row-${listUrl.replace(/\./g, '-')}`
|
||||
}
|
||||
|
||||
const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
|
||||
const { chainId } = useWeb3React()
|
||||
const listsByUrl = useAppSelector((state) => state.lists.byUrl)
|
||||
const dispatch = useAppDispatch()
|
||||
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]
|
||||
|
||||
const activeTokensOnThisChain = useMemo(() => {
|
||||
if (!list || !chainId) {
|
||||
return 0
|
||||
}
|
||||
return list.tokens.reduce((acc, cur) => (cur.chainId === chainId ? acc + 1 : acc), 0)
|
||||
}, [chainId, list])
|
||||
|
||||
const theme = useTheme()
|
||||
const listColor = useListColor(list?.logoURI)
|
||||
const isActive = useIsListActive(listUrl)
|
||||
|
||||
const [open, toggle] = useToggle(false)
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>()
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>()
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'auto',
|
||||
strategy: 'fixed',
|
||||
modifiers: [{ name: 'offset', options: { offset: [8, 8] } }],
|
||||
})
|
||||
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
const handleAcceptListUpdate = useCallback(() => {
|
||||
if (!pending) return
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Update List from List Select',
|
||||
label: listUrl,
|
||||
})
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
}, [dispatch, listUrl, pending])
|
||||
|
||||
const handleRemoveList = useCallback(() => {
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Start Remove List',
|
||||
label: listUrl,
|
||||
})
|
||||
if (window.prompt(t`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Confirm Remove List',
|
||||
label: listUrl,
|
||||
})
|
||||
dispatch(removeList(listUrl))
|
||||
}
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
const handleEnableList = useCallback(() => {
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Enable List',
|
||||
label: listUrl,
|
||||
})
|
||||
dispatch(enableList(listUrl))
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
const handleDisableList = useCallback(() => {
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Disable List',
|
||||
label: listUrl,
|
||||
})
|
||||
dispatch(disableList(listUrl))
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
if (!list) return null
|
||||
|
||||
return (
|
||||
<RowWrapper
|
||||
active={isActive}
|
||||
hasActiveTokens={activeTokensOnThisChain > 0}
|
||||
bgColor={listColor}
|
||||
key={listUrl}
|
||||
id={listUrlRowHTMLId(listUrl)}
|
||||
>
|
||||
{list.logoURI ? (
|
||||
<ListLogo size="40px" style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
|
||||
) : (
|
||||
<div style={{ width: '24px', height: '24px', marginRight: '1rem' }} />
|
||||
)}
|
||||
<Column style={{ flex: '1' }}>
|
||||
<Row>
|
||||
<StyledTitleText active={isActive}>{list.name}</StyledTitleText>
|
||||
</Row>
|
||||
<RowFixed mt="4px">
|
||||
<StyledListUrlText active={isActive} mr="6px">
|
||||
<Trans>{activeTokensOnThisChain} tokens</Trans>
|
||||
</StyledListUrlText>
|
||||
<StyledMenu ref={node as any}>
|
||||
<ButtonEmpty onClick={toggle} ref={setReferenceElement} padding="0">
|
||||
<Settings stroke={isActive ? theme.deprecated_bg1 : theme.deprecated_text1} size={12} />
|
||||
</ButtonEmpty>
|
||||
{open && (
|
||||
<PopoverContainer show={true} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
|
||||
<div>{list && listVersionLabel(list.version)}</div>
|
||||
<SeparatorDark />
|
||||
<ExternalLink href={`https://tokenlists.org/token-list?url=${listUrl}`}>
|
||||
<Trans>View list</Trans>
|
||||
</ExternalLink>
|
||||
<UnpaddedLinkStyledButton onClick={handleRemoveList} disabled={Object.keys(listsByUrl).length === 1}>
|
||||
<Trans>Remove list</Trans>
|
||||
</UnpaddedLinkStyledButton>
|
||||
{pending && (
|
||||
<UnpaddedLinkStyledButton onClick={handleAcceptListUpdate}>
|
||||
<Trans>Update list</Trans>
|
||||
</UnpaddedLinkStyledButton>
|
||||
)}
|
||||
</PopoverContainer>
|
||||
)}
|
||||
</StyledMenu>
|
||||
</RowFixed>
|
||||
</Column>
|
||||
<Toggle
|
||||
isActive={isActive}
|
||||
bgColor={listColor}
|
||||
toggle={() => {
|
||||
isActive ? handleDisableList() : handleEnableList()
|
||||
}}
|
||||
/>
|
||||
</RowWrapper>
|
||||
)
|
||||
})
|
||||
|
||||
const ListContainer = styled.div`
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
export function ManageLists({
|
||||
setModalView,
|
||||
setImportList,
|
||||
setListUrl,
|
||||
}: {
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
setImportList: (list: TokenList) => void
|
||||
setListUrl: (url: string) => void
|
||||
}) {
|
||||
const { chainId } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
|
||||
const [listUrlInput, setListUrlInput] = useState<string>('')
|
||||
|
||||
const lists = useAllLists()
|
||||
|
||||
const tokenCountByListName = useMemo<Record<string, number>>(
|
||||
() =>
|
||||
Object.values(lists).reduce((acc, { current: list }) => {
|
||||
if (!list) {
|
||||
return acc
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
[list.name]: list.tokens.reduce((count: number, token) => (token.chainId === chainId ? count + 1 : count), 0),
|
||||
}
|
||||
}, {}),
|
||||
[chainId, lists]
|
||||
)
|
||||
|
||||
// sort by active but only if not visible
|
||||
const activeListUrls = useActiveListUrls()
|
||||
|
||||
const handleInput = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setListUrlInput(e.target.value)
|
||||
}, [])
|
||||
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
const validUrl: boolean = useMemo(() => {
|
||||
return uriToHttp(listUrlInput).length > 0 || Boolean(parseENSAddress(listUrlInput))
|
||||
}, [listUrlInput])
|
||||
|
||||
const sortedLists = useMemo(() => {
|
||||
const listUrls = Object.keys(lists)
|
||||
return listUrls
|
||||
.filter((listUrl) => {
|
||||
// only show loaded lists, hide unsupported lists
|
||||
return Boolean(lists[listUrl].current) && !Boolean(UNSUPPORTED_LIST_URLS.includes(listUrl))
|
||||
})
|
||||
.sort((listUrlA, listUrlB) => {
|
||||
const { current: listA } = lists[listUrlA]
|
||||
const { current: listB } = lists[listUrlB]
|
||||
|
||||
// first filter on active lists
|
||||
if (activeListUrls?.includes(listUrlA) && !activeListUrls?.includes(listUrlB)) {
|
||||
return -1
|
||||
}
|
||||
if (!activeListUrls?.includes(listUrlA) && activeListUrls?.includes(listUrlB)) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (listA && listB) {
|
||||
if (tokenCountByListName[listA.name] > tokenCountByListName[listB.name]) {
|
||||
return -1
|
||||
}
|
||||
if (tokenCountByListName[listA.name] < tokenCountByListName[listB.name]) {
|
||||
return 1
|
||||
}
|
||||
return listA.name.toLowerCase() < listB.name.toLowerCase()
|
||||
? -1
|
||||
: listA.name.toLowerCase() === listB.name.toLowerCase()
|
||||
? 0
|
||||
: 1
|
||||
}
|
||||
if (listA) return -1
|
||||
if (listB) return 1
|
||||
return 0
|
||||
})
|
||||
}, [lists, activeListUrls, tokenCountByListName])
|
||||
|
||||
// temporary fetched list for import flow
|
||||
const [tempList, setTempList] = useState<TokenList>()
|
||||
const [addError, setAddError] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchTempList() {
|
||||
fetchList(listUrlInput, false)
|
||||
.then((list) => setTempList(list))
|
||||
.catch(() => setAddError(t`Error importing list`))
|
||||
}
|
||||
// if valid url, fetch details for card
|
||||
if (validUrl) {
|
||||
fetchTempList()
|
||||
} else {
|
||||
setTempList(undefined)
|
||||
listUrlInput !== '' && setAddError(t`Enter valid list location`)
|
||||
}
|
||||
|
||||
// reset error
|
||||
if (listUrlInput === '') {
|
||||
setAddError(undefined)
|
||||
}
|
||||
}, [fetchList, listUrlInput, validUrl])
|
||||
|
||||
// check if list is already imported
|
||||
const isImported = Object.keys(lists).includes(listUrlInput)
|
||||
|
||||
// set list values and have parent modal switch to import list view
|
||||
const handleImport = useCallback(() => {
|
||||
if (!tempList) return
|
||||
setImportList(tempList)
|
||||
setModalView(CurrencyModalView.importList)
|
||||
setListUrl(listUrlInput)
|
||||
}, [listUrlInput, setImportList, setListUrl, setModalView, tempList])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn gap="14px">
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="list-add-input"
|
||||
placeholder={t`https:// or ipfs:// or ENS name`}
|
||||
value={listUrlInput}
|
||||
onChange={handleInput}
|
||||
/>
|
||||
</Row>
|
||||
{addError ? (
|
||||
<ThemedText.DeprecatedError title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
|
||||
{addError}
|
||||
</ThemedText.DeprecatedError>
|
||||
) : null}
|
||||
</PaddedColumn>
|
||||
{tempList && (
|
||||
<PaddedColumn style={{ paddingTop: 0 }}>
|
||||
<Card backgroundColor={theme.deprecated_bg2} padding="12px 20px">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
{tempList.logoURI && <ListLogo logoURI={tempList.logoURI} size="40px" />}
|
||||
<AutoColumn gap="4px" style={{ marginLeft: '20px' }}>
|
||||
<ThemedText.DeprecatedBody fontWeight={600}>{tempList.name}</ThemedText.DeprecatedBody>
|
||||
<ThemedText.DeprecatedMain fontSize={'12px'}>
|
||||
<Trans>{tempList.tokens.length} tokens</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</AutoColumn>
|
||||
</RowFixed>
|
||||
{isImported ? (
|
||||
<RowFixed>
|
||||
<IconWrapper stroke={theme.deprecated_text2} size="16px" marginRight={'10px'}>
|
||||
<CheckCircle />
|
||||
</IconWrapper>
|
||||
<ThemedText.DeprecatedBody color={theme.deprecated_text2}>
|
||||
<Trans>Loaded</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</RowFixed>
|
||||
) : (
|
||||
<ButtonPrimary
|
||||
style={{ fontSize: '14px' }}
|
||||
padding="6px 8px"
|
||||
width="fit-content"
|
||||
onClick={handleImport}
|
||||
>
|
||||
<Trans>Import</Trans>
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</RowBetween>
|
||||
</Card>
|
||||
</PaddedColumn>
|
||||
)}
|
||||
<Separator />
|
||||
<ListContainer>
|
||||
<AutoColumn gap="md">
|
||||
{sortedLists.map((listUrl) => (
|
||||
<ListRow key={listUrl} listUrl={listUrl} />
|
||||
))}
|
||||
</AutoColumn>
|
||||
</ListContainer>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
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} />}
|
||||
|
@ -1,6 +1,6 @@
|
||||
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]
|
||||
|
@ -1,5 +1,7 @@
|
||||
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,17 +16,36 @@ 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 {
|
||||
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."
|
||||
/>
|
||||
)
|
||||
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}
|
||||
_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 }
|
||||
@ -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))
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,10 +1,9 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
@ -1,26 +1,21 @@
|
||||
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user