Commit a186833b authored by Moody Salem's avatar Moody Salem Committed by GitHub

fix(L2): enable optimism token list when switched to optimism (#2036)

* add title text and opacity variability to token lists

* add optimism token list

* show tokens from active lists

* sort up token lists
with tokens on the current chain

* fix up some type issues

prune out chainId changes

* clean up leftover any

* refactor token count mechanism

* handle plurals in title text string

* new combineMaps implementation

* remove custom plural

* address a couple nits

* show the number of tokens on current chain
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: default avatarJustin Domingue <judo@uniswap.org>
parent aa06db76
...@@ -48,7 +48,7 @@ export const OptimismWrapperBackgroundLightMode = css` ...@@ -48,7 +48,7 @@ export const OptimismWrapperBackgroundLightMode = css`
background: radial-gradient(92% 105% at 50% 7%, rgba(255, 58, 212, 0.04) 0%, rgba(255, 255, 255, 0.03) 100%), background: radial-gradient(92% 105% at 50% 7%, rgba(255, 58, 212, 0.04) 0%, rgba(255, 255, 255, 0.03) 100%),
radial-gradient(100% 97% at 0% 12%, rgba(235, 0, 255, 0.1) 0%, rgba(243, 19, 19, 0.1) 100%), hsla(0, 0%, 100%, 0.5); radial-gradient(100% 97% at 0% 12%, rgba(235, 0, 255, 0.1) 0%, rgba(243, 19, 19, 0.1) 100%), hsla(0, 0%, 100%, 0.5);
` `
const RootWrapper = styled.div<{ chainId: number; darkMode: boolean; logoUrl: string }>` const RootWrapper = styled.div<{ chainId: SupportedChainId; darkMode: boolean; logoUrl: string }>`
${({ chainId, darkMode }) => ${({ chainId, darkMode }) =>
[SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId) [SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
? darkMode ? darkMode
......
import { memo, useCallback, useMemo, useRef, useState, useEffect } from 'react' import { t, Trans } from '@lingui/macro'
import { Settings, CheckCircle } from 'react-feather' import { TokenList } from '@uniswap/token-lists'
import Card from 'components/Card'
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
import { useListColor } from 'hooks/useColor'
import { useActiveWeb3React } from 'hooks/web3'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { CheckCircle, Settings } from 'react-feather'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { usePopper } from 'react-popper' import { usePopper } from 'react-popper'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { useFetchListCallback } from '../../hooks/useFetchListCallback' import { useFetchListCallback } from '../../hooks/useFetchListCallback'
import { useOnClickOutside } from '../../hooks/useOnClickOutside' import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import { TokenList } from '@uniswap/token-lists' import useTheme from '../../hooks/useTheme'
import { t, Trans } from '@lingui/macro'
import useToggle from '../../hooks/useToggle' import useToggle from '../../hooks/useToggle'
import { acceptListUpdate, removeList, disableList, enableList } from '../../state/lists/actions' import { acceptListUpdate, disableList, enableList, removeList } from '../../state/lists/actions'
import { useIsListActive, useAllLists, useActiveListUrls } from '../../state/lists/hooks' import { useActiveListUrls, useAllLists, useIsListActive } from '../../state/lists/hooks'
import { ExternalLink, LinkStyledButton, TYPE, IconWrapper } from '../../theme' import { ExternalLink, IconWrapper, LinkStyledButton, TYPE } from '../../theme'
import listVersionLabel from '../../utils/listVersionLabel' import listVersionLabel from '../../utils/listVersionLabel'
import { parseENSAddress } from '../../utils/parseENSAddress' import { parseENSAddress } from '../../utils/parseENSAddress'
import uriToHttp from '../../utils/uriToHttp' import uriToHttp from '../../utils/uriToHttp'
import { ButtonEmpty, ButtonPrimary } from '../Button' import { ButtonEmpty, ButtonPrimary } from '../Button'
import Column, { AutoColumn } from '../Column' import Column, { AutoColumn } from '../Column'
import ListLogo from '../ListLogo' import ListLogo from '../ListLogo'
import Row, { RowFixed, RowBetween } from '../Row' import Row, { RowBetween, RowFixed } from '../Row'
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
import { useListColor } from 'hooks/useColor'
import useTheme from '../../hooks/useTheme'
import ListToggle from '../Toggle/ListToggle' import ListToggle from '../Toggle/ListToggle'
import Card from 'components/Card'
import { CurrencyModalView } from './CurrencySearchModal' import { CurrencyModalView } from './CurrencySearchModal'
import { UNSUPPORTED_LIST_URLS } from 'constants/lists' import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
const Wrapper = styled(Column)` const Wrapper = styled(Column)`
width: 100%;
height: 100%; height: 100%;
` `
...@@ -80,8 +78,9 @@ const StyledListUrlText = styled(TYPE.main)<{ active: boolean }>` ...@@ -80,8 +78,9 @@ const StyledListUrlText = styled(TYPE.main)<{ active: boolean }>`
color: ${({ theme, active }) => (active ? theme.white : theme.text2)}; color: ${({ theme, active }) => (active ? theme.white : theme.text2)};
` `
const RowWrapper = styled(Row)<{ bgColor: string; active: boolean }>` const RowWrapper = styled(Row)<{ bgColor: string; active: boolean; hasActiveTokens: boolean }>`
background-color: ${({ bgColor, active, theme }) => (active ? bgColor ?? 'transparent' : theme.bg2)}; background-color: ${({ bgColor, active, theme }) => (active ? bgColor ?? 'transparent' : theme.bg2)};
opacity: ${({ hasActiveTokens }) => (hasActiveTokens ? 1 : 0.4)};
transition: 200ms; transition: 200ms;
align-items: center; align-items: center;
padding: 1rem; padding: 1rem;
...@@ -93,10 +92,18 @@ function listUrlRowHTMLId(listUrl: string) { ...@@ -93,10 +92,18 @@ function listUrlRowHTMLId(listUrl: string) {
} }
const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) { const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
const { chainId } = useActiveWeb3React()
const listsByUrl = useAppSelector((state) => state.lists.byUrl) const listsByUrl = useAppSelector((state) => state.lists.byUrl)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl] 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 theme = useTheme()
const listColor = useListColor(list?.logoURI) const listColor = useListColor(list?.logoURI)
const isActive = useIsListActive(listUrl) const isActive = useIsListActive(listUrl)
...@@ -130,7 +137,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) { ...@@ -130,7 +137,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
action: 'Start Remove List', action: 'Start Remove List',
label: listUrl, label: listUrl,
}) })
if (window.prompt(`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) { if (window.prompt(t`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
ReactGA.event({ ReactGA.event({
category: 'Lists', category: 'Lists',
action: 'Confirm Remove List', action: 'Confirm Remove List',
...@@ -161,7 +168,13 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) { ...@@ -161,7 +168,13 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
if (!list) return null if (!list) return null
return ( return (
<RowWrapper active={isActive} bgColor={listColor} key={listUrl} id={listUrlRowHTMLId(listUrl)}> <RowWrapper
active={isActive}
hasActiveTokens={activeTokensOnThisChain > 0}
bgColor={listColor}
key={listUrl}
id={listUrlRowHTMLId(listUrl)}
>
{list.logoURI ? ( {list.logoURI ? (
<ListLogo size="40px" style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} /> <ListLogo size="40px" style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
) : ( ) : (
...@@ -173,7 +186,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) { ...@@ -173,7 +186,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
</Row> </Row>
<RowFixed mt="4px"> <RowFixed mt="4px">
<StyledListUrlText active={isActive} mr="6px"> <StyledListUrlText active={isActive} mr="6px">
<Trans>{list.tokens.length} tokens</Trans> <Trans>{activeTokensOnThisChain} tokens</Trans>
</StyledListUrlText> </StyledListUrlText>
<StyledMenu ref={node as any}> <StyledMenu ref={node as any}>
<ButtonEmpty onClick={toggle} ref={setReferenceElement} padding="0"> <ButtonEmpty onClick={toggle} ref={setReferenceElement} padding="0">
...@@ -226,20 +239,29 @@ export function ManageLists({ ...@@ -226,20 +239,29 @@ export function ManageLists({
setImportList: (list: TokenList) => void setImportList: (list: TokenList) => void
setListUrl: (url: string) => void setListUrl: (url: string) => void
}) { }) {
const { chainId } = useActiveWeb3React()
const theme = useTheme() const theme = useTheme()
const [listUrlInput, setListUrlInput] = useState<string>('') const [listUrlInput, setListUrlInput] = useState<string>('')
const lists = useAllLists() 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 // sort by active but only if not visible
const activeListUrls = useActiveListUrls() const activeListUrls = useActiveListUrls()
const [activeCopy, setActiveCopy] = useState<string[] | undefined>()
useEffect(() => {
if (!activeCopy && activeListUrls) {
setActiveCopy(activeListUrls)
}
}, [activeCopy, activeListUrls])
const handleInput = useCallback((e) => { const handleInput = useCallback((e) => {
setListUrlInput(e.target.value) setListUrlInput(e.target.value)
...@@ -258,30 +280,36 @@ export function ManageLists({ ...@@ -258,30 +280,36 @@ export function ManageLists({
// only show loaded lists, hide unsupported lists // only show loaded lists, hide unsupported lists
return Boolean(lists[listUrl].current) && !Boolean(UNSUPPORTED_LIST_URLS.includes(listUrl)) return Boolean(lists[listUrl].current) && !Boolean(UNSUPPORTED_LIST_URLS.includes(listUrl))
}) })
.sort((u1, u2) => { .sort((listUrlA, listUrlB) => {
const { current: l1 } = lists[u1] const { current: listA } = lists[listUrlA]
const { current: l2 } = lists[u2] const { current: listB } = lists[listUrlB]
// first filter on active lists // first filter on active lists
if (activeCopy?.includes(u1) && !activeCopy?.includes(u2)) { if (activeListUrls?.includes(listUrlA) && !activeListUrls?.includes(listUrlB)) {
return -1 return -1
} }
if (!activeCopy?.includes(u1) && activeCopy?.includes(u2)) { if (!activeListUrls?.includes(listUrlA) && activeListUrls?.includes(listUrlB)) {
return 1 return 1
} }
if (l1 && l2) { if (listA && listB) {
return l1.name.toLowerCase() < l2.name.toLowerCase() 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 ? -1
: l1.name.toLowerCase() === l2.name.toLowerCase() : listA.name.toLowerCase() === listB.name.toLowerCase()
? 0 ? 0
: 1 : 1
} }
if (l1) return -1 if (listA) return -1
if (l2) return 1 if (listB) return 1
return 0 return 0
}) })
}, [lists, activeCopy]) }, [lists, activeListUrls, tokenCountByListName])
// temporary fetched list for import flow // temporary fetched list for import flow
const [tempList, setTempList] = useState<TokenList>() const [tempList, setTempList] = useState<TokenList>()
......
...@@ -19,7 +19,6 @@ export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST] ...@@ -19,7 +19,6 @@ export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST]
export const DEFAULT_LIST_OF_LISTS: string[] = [ export const DEFAULT_LIST_OF_LISTS: string[] = [
COMPOUND_LIST, COMPOUND_LIST,
AAVE_LIST, AAVE_LIST,
OPTIMISM_LIST,
CMC_ALL_LIST, CMC_ALL_LIST,
CMC_STABLECOIN, CMC_STABLECOIN,
UMA_LIST, UMA_LIST,
...@@ -28,6 +27,7 @@ export const DEFAULT_LIST_OF_LISTS: string[] = [ ...@@ -28,6 +27,7 @@ export const DEFAULT_LIST_OF_LISTS: string[] = [
ROLL_LIST, ROLL_LIST,
COINGECKO_LIST, COINGECKO_LIST,
KLEROS_LIST, KLEROS_LIST,
OPTIMISM_LIST,
GEMINI_LIST, GEMINI_LIST,
...UNSUPPORTED_LIST_URLS, // need to load unsupported tokens as well ...UNSUPPORTED_LIST_URLS, // need to load unsupported tokens as well
] ]
......
...@@ -112,7 +112,12 @@ export const COMMON_BASES: ChainCurrencyList = { ...@@ -112,7 +112,12 @@ export const COMMON_BASES: ChainCurrencyList = {
ExtendedEther.onChain(SupportedChainId.ARBITRUM_ONE), ExtendedEther.onChain(SupportedChainId.ARBITRUM_ONE),
WETH9_EXTENDED[SupportedChainId.ARBITRUM_ONE], WETH9_EXTENDED[SupportedChainId.ARBITRUM_ONE],
], ],
[SupportedChainId.ARBITRUM_RINKEBY]: [
ExtendedEther.onChain(SupportedChainId.ARBITRUM_RINKEBY),
WETH9_EXTENDED[SupportedChainId.ARBITRUM_RINKEBY],
],
[SupportedChainId.OPTIMISM]: [ExtendedEther.onChain(SupportedChainId.OPTIMISM)], [SupportedChainId.OPTIMISM]: [ExtendedEther.onChain(SupportedChainId.OPTIMISM)],
[SupportedChainId.OPTIMISTIC_KOVAN]: [ExtendedEther.onChain(SupportedChainId.OPTIMISTIC_KOVAN)],
} }
// used to construct the list of all pairs we consider by default in the frontend // used to construct the list of all pairs we consider by default in the frontend
......
...@@ -46,14 +46,15 @@ export function useAllLists(): AppState['lists']['byUrl'] { ...@@ -46,14 +46,15 @@ export function useAllLists(): AppState['lists']['byUrl'] {
return useAppSelector((state) => state.lists.byUrl) return useAppSelector((state) => state.lists.byUrl)
} }
function combineMaps(map1: TokenAddressMap, map2: TokenAddressMap): TokenAddressMap { export function combineMaps(map1: TokenAddressMap, map2: TokenAddressMap): TokenAddressMap {
return { const chainIds = Object.keys({ ...map1, ...map2 }).map((id) => parseInt(id))
[1]: { ...map1[1], ...map2[1] }, return chainIds.reduce(
[4]: { ...map1[4], ...map2[4] }, (acc, chainId) => ({
[3]: { ...map1[3], ...map2[3] }, ...acc,
[42]: { ...map1[42], ...map2[42] }, [chainId]: { ...map2[chainId], ...map1[chainId] },
[5]: { ...map1[5], ...map2[5] }, }),
} {}
)
} }
// merge tokens contained within lists from urls // merge tokens contained within lists from urls
......
import { useAllLists } from 'state/lists/hooks'
import { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists' import { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists'
import { SupportedChainId } from 'constants/chains'
import { OPTIMISM_LIST, UNSUPPORTED_LIST_URLS } from 'constants/lists'
import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import { useAppDispatch } from 'state/hooks'
import { useActiveWeb3React } from '../../hooks/web3' import { useAllLists } from 'state/lists/hooks'
import { useFetchListCallback } from '../../hooks/useFetchListCallback' import { useFetchListCallback } from '../../hooks/useFetchListCallback'
import useInterval from '../../hooks/useInterval' import useInterval from '../../hooks/useInterval'
import useIsWindowVisible from '../../hooks/useIsWindowVisible' import useIsWindowVisible from '../../hooks/useIsWindowVisible'
import { acceptListUpdate } from './actions' import { useActiveWeb3React } from '../../hooks/web3'
import { acceptListUpdate, enableList } from './actions'
import { useActiveListUrls } from './hooks' import { useActiveListUrls } from './hooks'
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
import { useAppDispatch } from 'state/hooks'
export default function Updater(): null { export default function Updater(): null {
const { library } = useActiveWeb3React() const { chainId, library } = useActiveWeb3React()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const isWindowVisible = useIsWindowVisible() const isWindowVisible = useIsWindowVisible()
...@@ -28,6 +28,11 @@ export default function Updater(): null { ...@@ -28,6 +28,11 @@ export default function Updater(): null {
) )
}, [fetchList, isWindowVisible, lists]) }, [fetchList, isWindowVisible, lists])
useEffect(() => {
if (chainId && [SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)) {
dispatch(enableList(OPTIMISM_LIST))
}
}, [chainId, dispatch])
// fetch all lists every 10 minutes, but only after we initialize library // fetch all lists every 10 minutes, but only after we initialize library
useInterval(fetchAllListsCallback, library ? 1000 * 60 * 10 : null) useInterval(fetchAllListsCallback, library ? 1000 * 60 * 10 : 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