feat: update token icon sourcing (#5208)

* initial setup
* working version
* optimized image quality
* refactoring file structure
* updating backup logos
* cleaning up code
* refactored file structure
* fixing state update issue
* updated test
* fixed currency logo bug
* updated image background color
* finished pr comments
* fixed additional PR comments
This commit is contained in:
cartcrom 2022-11-17 16:25:04 -05:00 committed by GitHub
parent 7ce58b55a1
commit 3f6dc180cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 299 additions and 190 deletions

@ -5,7 +5,7 @@ import styled, { css } from 'styled-components/macro'
import { nativeOnChain } from '../../constants/tokens'
import { useCurrency } from '../../hooks/Tokens'
import CurrencyLogo from '../CurrencyLogo'
import CurrencyLogo from '../Logo/CurrencyLogo'
const CurrencyWrap = styled.div`
position: relative;

@ -6,6 +6,7 @@ import { Pair } from '@uniswap/v2-sdk'
import { useWeb3React } from '@web3-react/core'
import { AutoColumn } from 'components/Column'
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { isSupportedChain } from 'constants/chains'
import { darken } from 'polished'
import { ReactNode, useCallback, useState } from 'react'
@ -18,7 +19,6 @@ import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import { useCurrencyBalance } from '../../state/connection/hooks'
import { ThemedText } from '../../theme'
import { ButtonGray } from '../Button'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { Input as NumericalInput } from '../NumericalInput'
import { RowBetween, RowFixed } from '../Row'

@ -18,8 +18,8 @@ import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import { useCurrencyBalance } from '../../state/connection/hooks'
import { ThemedText } from '../../theme'
import { ButtonGray } from '../Button'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import CurrencyLogo from '../Logo/CurrencyLogo'
import { Input as NumericalInput } from '../NumericalInput'
import { RowBetween, RowFixed } from '../Row'
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'

@ -1,51 +0,0 @@
import { Currency } from '@uniswap/sdk-core'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import React, { useMemo } from 'react'
import styled from 'styled-components/macro'
import Logo from '../Logo'
const StyledLogo = styled(Logo)<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
background: radial-gradient(white 50%, #ffffff00 calc(75% + 1px), #ffffff00 100%);
border-radius: 50%;
-mox-box-shadow: 0 0 1px black;
-webkit-box-shadow: 0 0 1px black;
box-shadow: 0 0 1px black;
border: 0px solid rgba(255, 255, 255, 0);
`
const StyledNativeLogo = styled(StyledLogo)`
-mox-box-shadow: 0 0 1px white;
-webkit-box-shadow: 0 0 1px white;
box-shadow: 0 0 1px white;
`
export default function CurrencyLogo({
currency,
symbol,
size = '24px',
style,
src,
...rest
}: {
currency?: Currency | null
symbol?: string | null
size?: string
style?: React.CSSProperties
src?: string | null
}) {
const logoURIs = useCurrencyLogoURIs(currency)
const srcs = useMemo(() => (src ? [src, ...logoURIs] : logoURIs), [src, logoURIs])
const props = {
alt: `${currency?.symbol ?? 'token'} logo`,
size,
srcs,
symbol: symbol ?? currency?.symbol,
style,
...rest,
}
return currency?.isNative ? <StyledNativeLogo {...props} /> : <StyledLogo {...props} />
}

@ -1,7 +1,7 @@
import { Currency } from '@uniswap/sdk-core'
import styled from 'styled-components/macro'
import CurrencyLogo from '../CurrencyLogo'
import CurrencyLogo from '../Logo/CurrencyLogo'
const Wrapper = styled.div<{ margin: boolean; sizeraw: number }>`
position: relative;

@ -0,0 +1,68 @@
import { SupportedChainId } from 'constants/chains'
import useTokenLogoSource from 'hooks/useAssetLogoSource'
import React from 'react'
import styled from 'styled-components/macro'
export const MissingImageLogo = styled.div<{ size?: string }>`
--size: ${({ size }) => size};
border-radius: 100px;
color: ${({ theme }) => theme.textPrimary};
background-color: ${({ theme }) => theme.backgroundInteractive};
font-size: calc(var(--size) / 3);
font-weight: 500;
height: ${({ size }) => size ?? '24px'};
line-height: ${({ size }) => size ?? '24px'};
text-align: center;
width: ${({ size }) => size ?? '24px'};
`
const LogoImage = styled.img<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
background: radial-gradient(white 60%, #ffffff00 calc(70% + 1px));
border-radius: 50%;
box-shadow: 0 0 1px white;
`
export type AssetLogoBaseProps = {
symbol?: string | null
backupImg?: string | null
size?: string
style?: React.CSSProperties
}
type AssetLogoProps = AssetLogoBaseProps & { isNative?: boolean; address?: string | null; chainId?: number }
// TODO(cartcrom): add prop to optionally render an L2Icon w/ the logo
/**
* Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert
*/
export default function AssetLogo({
isNative,
address,
chainId = SupportedChainId.MAINNET,
symbol,
backupImg,
size = '24px',
style,
...rest
}: AssetLogoProps) {
const imageProps = {
alt: `${symbol ?? 'token'} logo`,
size,
style,
...rest,
}
const [src, nextSrc] = useTokenLogoSource(address, chainId, isNative, backupImg)
if (src) {
return <LogoImage {...imageProps} src={src} onError={nextSrc} />
} else {
return (
<MissingImageLogo size={size}>
{/* use only first 3 characters of Symbol for design reasons */}
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)
}
}

@ -0,0 +1,21 @@
import { Currency } from '@uniswap/sdk-core'
import { TokenInfo } from '@uniswap/token-lists'
import AssetLogo, { AssetLogoBaseProps } from './AssetLogo'
export default function CurrencyLogo(
props: AssetLogoBaseProps & {
currency?: Currency | null
}
) {
return (
<AssetLogo
isNative={props.currency?.isNative}
chainId={props.currency?.chainId}
address={props.currency?.wrapped.address}
symbol={props.symbol ?? props.currency?.symbol}
backupImg={(props.currency as TokenInfo)?.logoURI}
{...props}
/>
)
}

@ -0,0 +1,25 @@
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { TokenQueryData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
import AssetLogo, { AssetLogoBaseProps } from './AssetLogo'
export default function QueryTokenLogo(
props: AssetLogoBaseProps & {
token?: TopToken | TokenQueryData
}
) {
const chainId = props.token?.chain ? CHAIN_NAME_TO_CHAIN_ID[props.token?.chain] : undefined
return (
<AssetLogo
isNative={props.token?.address === NATIVE_CHAIN_ID}
chainId={chainId}
address={props.token?.address}
symbol={props.token?.symbol}
backupImg={props.token?.project?.logoUrl}
{...props}
/>
)
}

@ -1,55 +0,0 @@
import { useState } from 'react'
import { ImageProps } from 'rebass'
import styled from 'styled-components/macro'
const BAD_SRCS: { [tokenAddress: string]: true } = {}
interface LogoProps extends Pick<ImageProps, 'style' | 'alt' | 'className'> {
srcs: string[]
symbol?: string
size?: string
}
const MissingImageLogo = styled.div<{ size?: string }>`
--size: ${({ size }) => size};
border-radius: 100px;
color: ${({ theme }) => theme.textPrimary};
background-color: ${({ theme }) => theme.backgroundInteractive};
font-size: calc(var(--size) / 3);
font-weight: 500;
height: ${({ size }) => size ?? '24px'};
line-height: ${({ size }) => size ?? '24px'};
text-align: center;
width: ${({ size }) => size ?? '24px'};
`
/**
* Renders an image by sequentially trying a list of URIs, and then eventually a fallback triangle alert
*/
export default function Logo({ srcs, alt, style, size, symbol, ...rest }: LogoProps) {
const [, refresh] = useState<number>(0)
const src: string | undefined = srcs.find((src) => !BAD_SRCS[src])
if (src) {
return (
<img
{...rest}
alt={alt}
src={src}
style={style}
onError={() => {
if (src) BAD_SRCS[src] = true
refresh((i) => i + 1)
}}
/>
)
}
return (
<MissingImageLogo size={size}>
{/* use only first 3 characters of Symbol for design reasons */}
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)
}

@ -2,12 +2,13 @@ import { sendAnalyticsEvent } from '@uniswap/analytics'
import { EventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx'
import AssetLogo from 'components/Logo/AssetLogo'
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety'
import { getTokenDetailsURL } from 'graphql/data/util'
import uriToHttp from 'lib/utils/uriToHttp'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { VerifiedIcon } from 'nft/components/icons'
@ -18,10 +19,15 @@ import { ethNumberStandardFormatter } from 'nft/utils/currency'
import { putCommas } from 'nft/utils/putCommas'
import { useCallback, useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { formatDollar } from 'utils/formatNumbers'
import * as styles from './SearchBar.css'
const StyledLogoContainer = styled(LogoContainer)`
margin-right: 8px;
`
interface CollectionRowProps {
collection: GenieCollection
isHovered: boolean
@ -127,8 +133,6 @@ interface TokenRowProps {
}
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
const [brokenImage, setBrokenImage] = useState(false)
const [loaded, setLoaded] = useState(false)
const addToSearchHistory = useSearchHistory(
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
)
@ -167,21 +171,17 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
>
<Row style={{ width: '65%' }}>
{!brokenImage && token.logoURI ? (
<LogoContainer>
<Box
as="img"
src={token.logoURI.includes('ipfs://') ? uriToHttp(token.logoURI)[0] : token.logoURI}
alt={token.name}
className={clsx(loaded ? styles.suggestionImage : styles.imageHolder)}
onError={() => setBrokenImage(true)}
onLoad={() => setLoaded(true)}
/>
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
</LogoContainer>
) : (
<Box className={styles.imageHolder} />
)}
<StyledLogoContainer>
<AssetLogo
isNative={token.address === NATIVE_CHAIN_ID}
address={token.address}
chainId={token.chainId}
symbol={token.symbol}
size="36px"
backupImg={token.logoURI}
/>
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
</StyledLogoContainer>
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box>

@ -19,9 +19,9 @@ import { unwrappedToken } from '../../utils/unwrappedToken'
import { ButtonEmpty, ButtonPrimary, ButtonSecondary } from '../Button'
import { LightCard } from '../Card'
import { AutoColumn } from '../Column'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { CardNoise } from '../earn/styled'
import CurrencyLogo from '../Logo/CurrencyLogo'
import { AutoRow, RowBetween, RowFixed } from '../Row'
import { Dots } from '../swap/styleds'
import { FixedHeightRow } from '.'

@ -20,9 +20,9 @@ import { unwrappedToken } from '../../utils/unwrappedToken'
import { ButtonEmpty, ButtonPrimary, ButtonSecondary } from '../Button'
import { GrayCard, LightCard } from '../Card'
import { AutoColumn } from '../Column'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { CardNoise } from '../earn/styled'
import CurrencyLogo from '../Logo/CurrencyLogo'
import { AutoRow, RowBetween, RowFixed } from '../Row'
import { Dots } from '../swap/styleds'

@ -4,9 +4,9 @@ import { Position } from '@uniswap/v3-sdk'
import RangeBadge from 'components/Badge/RangeBadge'
import { LightCard } from 'components/Card'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import { Break } from 'components/earn/styled'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import RateToggle from 'components/RateToggle'
import { RowBetween, RowFixed } from 'components/Row'
import JSBI from 'jsbi'

@ -28,7 +28,7 @@ const multiRoute: RoutingDiagramEntry[] = [
]
jest.mock(
'components/CurrencyLogo',
'components/Logo/CurrencyLogo',
() =>
({ currency }: { currency: Currency }) =>
`CurrencyLogo currency=${currency.symbol}`

@ -3,8 +3,8 @@ import { Protocol } from '@uniswap/router-sdk'
import { Currency } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import Badge from 'components/Badge'
import CurrencyLogo from 'components/CurrencyLogo'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import Row, { AutoRow } from 'components/Row'
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'

@ -2,7 +2,7 @@ import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { Currency } from '@uniswap/sdk-core'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { AutoRow } from 'components/Row'
import { COMMON_BASES } from 'constants/routing'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'

@ -16,7 +16,7 @@ const mockCurrencyAmt = {
}
jest.mock(
'components/CurrencyLogo',
'components/Logo/CurrencyLogo',
() =>
({ currency }: { currency: Currency }) =>
`CurrencyLogo currency=${currency.symbol}`

@ -15,8 +15,8 @@ import { useCurrencyBalance } from '../../../state/connection/hooks'
import { WrappedTokenInfo } from '../../../state/lists/wrappedTokenInfo'
import { ThemedText } from '../../../theme'
import Column, { AutoColumn } from '../../Column'
import CurrencyLogo from '../../CurrencyLogo'
import Loader from '../../Loader'
import CurrencyLogo from '../../Logo/CurrencyLogo'
import Row, { RowFixed } from '../../Row'
import { MouseoverTooltip } from '../../Tooltip'
import { LoadingRows, MenuItem } from '../styleds'

@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import TokenSafetyLabel from 'components/TokenSafety/TokenSafetyLabel'
import { checkWarning, getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning } from 'constants/tokenSafety'
import { useToken } from 'hooks/Tokens'

@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import CurrencyLogo from 'components/CurrencyLogo'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import { formatToDecimal } from 'lib/utils/analytics'

@ -1,6 +1,7 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { TokenQueryData } from 'graphql/data/Token'
import { chainIdToBackendName } from 'graphql/data/util'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useRef } from 'react'
import { Twitter } from 'react-feather'
@ -63,12 +64,7 @@ const ShareAction = styled.div`
}
`
interface TokenInfo {
token: NonNullable<TokenQueryData>
isNative: boolean
}
export default function ShareButton(tokenInfo: TokenInfo) {
export default function ShareButton({ currency }: { currency: Currency }) {
const theme = useTheme()
const node = useRef<HTMLDivElement | null>(null)
const open = useModalIsOpen(ApplicationModal.SHARE)
@ -76,12 +72,16 @@ export default function ShareButton(tokenInfo: TokenInfo) {
useOnClickOutside(node, open ? toggleShare : undefined)
const positionX = (window.screen.width - TWITTER_WIDTH) / 2
const positionY = (window.screen.height - TWITTER_HEIGHT) / 2
const tokenAddress = tokenInfo.isNative ? NATIVE_CHAIN_ID : tokenInfo.token.address
const address = currency.isNative ? NATIVE_CHAIN_ID : currency.wrapped.address
const shareTweet = () => {
toggleShare()
window.open(
`https://twitter.com/intent/tweet?text=Check%20out%20${tokenInfo.token.name}%20(${tokenInfo.token.symbol})%20https://app.uniswap.org/%23/tokens/${tokenInfo.token.chain}/${tokenAddress}%20via%20@uniswap`,
`https://twitter.com/intent/tweet?text=Check%20out%20${currency.name}%20(${
currency.symbol
})%20https://app.uniswap.org/%23/tokens/${chainIdToBackendName(
currency.chainId
).toLowerCase()}/${address}%20via%20@uniswap`,
'newwindow',
`left=${positionX}, top=${positionY}, width=${TWITTER_WIDTH}, height=${TWITTER_HEIGHT}`
)

@ -1,8 +1,8 @@
import { Trans } from '@lingui/macro'
import { Trace } from '@uniswap/analytics'
import { PageName } from '@uniswap/analytics-events'
import { Currency, NativeCurrency, Token } from '@uniswap/sdk-core'
import CurrencyLogo from 'components/CurrencyLogo'
import { Currency, Token } from '@uniswap/sdk-core'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { AboutSection } from 'components/Tokens/TokenDetails/About'
import AddressSection from 'components/Tokens/TokenDetails/AddressSection'
import BalanceSummary from 'components/Tokens/TokenDetails/BalanceSummary'
@ -24,17 +24,14 @@ import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import Widget from 'components/Widget'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { DEFAULT_ERC20_DECIMALS, NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety'
import { TokenPriceQuery } from 'graphql/data/__generated__/TokenPriceQuery.graphql'
import { Chain, TokenQuery } from 'graphql/data/Token'
import { QueryToken, tokenQuery, TokenQueryData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens'
import { QueryToken, tokenQuery } from 'graphql/data/Token'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { useCallback, useMemo, useState, useTransition } from 'react'
import { ArrowLeft } from 'react-feather'
import { PreloadedQuery, usePreloadedQuery } from 'react-relay'
@ -53,15 +50,6 @@ const TokenActions = styled.div`
color: ${({ theme }) => theme.textSecondary};
`
export function useTokenLogoURI(token?: TokenQueryData | TopToken, nativeCurrency?: Token | NativeCurrency) {
const chainId = token ? CHAIN_NAME_TO_CHAIN_ID[token.chain] : SupportedChainId.MAINNET
return [
...useCurrencyLogoURIs(nativeCurrency),
...useCurrencyLogoURIs({ ...token, chainId }),
token?.project?.logoUrl,
][0]
}
type TokenDetailsProps = {
tokenAddress: string | undefined
chain: Chain
@ -85,12 +73,12 @@ export default function TokenDetails({
const isNative = tokenAddress === NATIVE_CHAIN_ID
const tokenQueryData = usePreloadedQuery(tokenQuery, tokenQueryReference).tokens?.[0]
const token = useMemo(() => {
if (isNative) return nativeCurrency
if (tokenQueryData) return new QueryToken(tokenQueryData)
return new Token(pageChainId, tokenAddress, DEFAULT_ERC20_DECIMALS)
}, [isNative, nativeCurrency, pageChainId, tokenAddress, tokenQueryData])
const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null
const isBlockedToken = tokenWarning?.canProceed === false
@ -133,7 +121,6 @@ export default function TokenDetails({
[continueSwap, setContinueSwap]
)
const logoSrc = useTokenLogoURI(tokenQueryData, isNative ? nativeCurrency : undefined)
const L2Icon = getChainInfo(pageChainId)?.circleLogoUrl
return (
@ -147,12 +134,7 @@ export default function TokenDetails({
<TokenInfoContainer>
<TokenNameCell>
<LogoContainer>
<CurrencyLogo
src={logoSrc}
size="32px"
symbol={isNative ? nativeCurrency?.symbol : token?.symbol}
currency={isNative ? nativeCurrency : token}
/>
<CurrencyLogo currency={token} size="32px" />
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
</LogoContainer>
{token?.name ?? <Trans>Name not found</Trans>}
@ -160,7 +142,7 @@ export default function TokenDetails({
</TokenNameCell>
<TokenActions>
{tokenQueryData?.name && tokenQueryData.symbol && tokenQueryData.address && (
<ShareButton token={tokenQueryData} isNative={!!nativeCurrency} />
<ShareButton currency={token} />
)}
</TokenActions>
</TokenInfoContainer>

@ -3,7 +3,7 @@ import { sendAnalyticsEvent } from '@uniswap/analytics'
import { EventName } from '@uniswap/analytics-events'
import { ParentSize } from '@visx/responsive'
import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo'
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
import { getChainInfo } from 'constants/chainInfo'
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
@ -31,7 +31,6 @@ import {
TokenSortMethod,
useSetSortMethod,
} from '../state'
import { useTokenLogoURI } from '../TokenDetails'
import InfoTip from '../TokenDetails/InfoTip'
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
@ -438,7 +437,8 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum'
const filterNetwork = lowercaseChainName.toUpperCase()
const L2Icon = getChainInfo(CHAIN_NAME_TO_CHAIN_ID[filterNetwork])?.circleLogoUrl
const chainId = CHAIN_NAME_TO_CHAIN_ID[filterNetwork]
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
const timePeriod = useAtomValue(filterTimeAtom)
const delta = token.market?.pricePercentChange?.value
const arrow = getDeltaArrow(delta)
@ -446,7 +446,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
const rank = sortAscending ? tokenListLength - tokenListIndex : tokenListIndex + 1
const exploreTokenSelectedEventProperties = {
chain_id: filterNetwork,
chain_id: chainId,
token_address: tokenAddress,
token_symbol: tokenSymbol,
token_list_index: tokenListIndex,
@ -469,7 +469,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
tokenInfo={
<ClickableName>
<LogoContainer>
<CurrencyLogo src={useTokenLogoURI(token)} symbol={tokenSymbol} />
<QueryTokenLogo token={token} />
<L2NetworkLogo networkUrl={L2Icon} />
</LogoContainer>
<TokenInfoCell>

@ -17,7 +17,7 @@ import { ButtonPrimary } from '../Button'
import { LightCard } from '../Card'
import { AutoColumn } from '../Column'
import { FiatValue } from '../CurrencyInputPanel/FiatValue'
import CurrencyLogo from '../CurrencyLogo'
import CurrencyLogo from '../Logo/CurrencyLogo'
import { RowBetween, RowFixed } from '../Row'
import TradePrice from '../swap/TradePrice'
import { AdvancedSwapDetails } from './AdvancedSwapDetails'

@ -4,7 +4,7 @@ import { useWeb3React } from '@web3-react/core'
import { ButtonEmpty } from 'components/Button'
import Card, { OutlineCard } from 'components/Card'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import Modal from 'components/Modal'
import { AutoRow, RowBetween } from 'components/Row'
import { useState } from 'react'

@ -0,0 +1,38 @@
import store from 'state'
import { DEFAULT_LIST_OF_LISTS } from './lists'
class TokenLogoLookupTable {
private dict: { [key: string]: string[] | undefined } = {}
private initialized = false
initialize() {
const dict: { [key: string]: string[] | undefined } = {}
DEFAULT_LIST_OF_LISTS.forEach((list) =>
store.getState().lists.byUrl[list].current?.tokens.forEach((token) => {
if (token.logoURI) {
const lowercaseAddress = token.address.toLowerCase()
const currentEntry = dict[lowercaseAddress]
if (currentEntry) {
currentEntry.push(token.logoURI)
} else {
dict[lowercaseAddress] = [token.logoURI]
}
}
})
)
this.dict = dict
this.initialized = true
}
getIcons(address?: string | null) {
if (!address) return undefined
if (!this.initialized) {
this.initialize()
}
return this.dict[address.toLowerCase()]
}
}
export default new TokenLogoLookupTable()

@ -61,14 +61,14 @@ export type TokenQueryData = NonNullable<TokenQuery$data['tokens']>[number]
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
export class QueryToken extends WrappedTokenInfo {
constructor(data: NonNullable<TokenQueryData>) {
constructor(data: NonNullable<TokenQueryData>, logoSrc?: string) {
super({
chainId: CHAIN_NAME_TO_CHAIN_ID[data.chain],
address: data.address,
decimals: data.decimals ?? DEFAULT_ERC20_DECIMALS,
symbol: data.symbol ?? '',
name: data.name ?? '',
logoURI: data.project?.logoUrl ?? undefined,
logoURI: logoSrc ?? data.project?.logoUrl ?? undefined,
})
}
}

@ -0,0 +1,81 @@
import TokenLogoLookupTable from 'constants/TokenLogoLookupTable'
import { chainIdToNetworkName, getNativeLogoURI } from 'lib/hooks/useCurrencyLogoURIs'
import uriToHttp from 'lib/utils/uriToHttp'
import { useCallback, useEffect, useState } from 'react'
import { isAddress } from 'utils'
export const BAD_SRCS: { [tokenAddress: string]: true } = {}
// Converts uri's into fetchable urls
function parseLogoSources(uris: string[]) {
const urls: string[] = []
uris.forEach((uri) => urls.push(...uriToHttp(uri)))
return urls
}
// Parses uri's, favors non-coingecko images, and improves coingecko logo quality
function prioritizeLogoSources(uris: string[]) {
const parsedUris = uris.map((uri) => uriToHttp(uri)).flat(1)
const preferredUris: string[] = []
// Consolidate duplicate coingecko urls into one fallback source
let coingeckoUrl: string | undefined = undefined
parsedUris.forEach((uri) => {
if (uri.startsWith('https://assets.coingecko')) {
if (!coingeckoUrl) {
coingeckoUrl = uri.replace(/small|thumb/g, 'large')
}
} else {
preferredUris.push(uri)
}
})
// Places coingecko urls in the back of the source array
return coingeckoUrl ? [...preferredUris, coingeckoUrl] : preferredUris
}
function getInitialUrl(address?: string | null, chainId?: number | null, isNative?: boolean) {
if (chainId && isNative) return getNativeLogoURI(chainId)
const networkName = chainId ? chainIdToNetworkName(chainId) : 'ethereum'
const checksummedAddress = isAddress(address)
if (checksummedAddress) {
return `https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/${networkName}/assets/${checksummedAddress}/logo.png`
} else {
return undefined
}
}
export default function useAssetLogoSource(
address?: string | null,
chainId?: number | null,
isNative?: boolean,
backupImg?: string | null
): [string | undefined, () => void] {
const [current, setCurrent] = useState<string | undefined>(getInitialUrl(address, chainId, isNative))
const [fallbackSrcs, setFallbackSrcs] = useState<string[] | undefined>(undefined)
useEffect(() => {
setCurrent(getInitialUrl(address, chainId, isNative))
setFallbackSrcs(undefined)
}, [address, chainId, isNative])
const nextSrc = useCallback(() => {
if (current) {
BAD_SRCS[current] = true
}
// Parses and stores logo sources from tokenlists if assets repo url fails
if (!fallbackSrcs) {
const uris = TokenLogoLookupTable.getIcons(address) ?? []
if (backupImg) uris.push(backupImg)
const tokenListIcons = prioritizeLogoSources(parseLogoSources(uris))
setCurrent(tokenListIcons.find((src) => !BAD_SRCS[src]))
setFallbackSrcs(tokenListIcons)
} else {
setCurrent(fallbackSrcs.find((src) => !BAD_SRCS[src]))
}
}, [current, fallbackSrcs, address, backupImg])
return [current, nextSrc]
}

@ -5,7 +5,7 @@ import { useMemo } from 'react'
import useENSContentHash from './useENSContentHash'
export default function useHttpLocations(uri: string | undefined): string[] {
export default function useHttpLocations(uri: string | undefined | null): string[] {
const ens = useMemo(() => (uri ? parseENSAddress(uri) : undefined), [uri])
const resolvedContentHash = useENSContentHash(ens?.ensName)
return useMemo(() => {

@ -10,7 +10,7 @@ import { isCelo, NATIVE_CHAIN_ID, nativeOnChain } from '../../constants/tokens'
type Network = 'ethereum' | 'arbitrum' | 'optimism' | 'polygon'
function chainIdToNetworkName(networkId: SupportedChainId): Network {
export function chainIdToNetworkName(networkId: SupportedChainId): Network {
switch (networkId) {
case SupportedChainId.MAINNET:
return 'ethereum'
@ -60,7 +60,7 @@ export default function useCurrencyLogoURIs(
isToken?: boolean
address?: string
chainId: number
logoURI?: string
logoURI?: string | null
}
| null
| undefined

@ -3,7 +3,7 @@ import { Currency, CurrencyAmount, Fraction, Percent } from '@uniswap/sdk-core'
import { Text } from 'rebass'
import { ButtonPrimary } from '../../components/Button'
import CurrencyLogo from '../../components/CurrencyLogo'
import CurrencyLogo from '../../components/Logo/CurrencyLogo'
import { RowBetween, RowFixed } from '../../components/Row'
import { Field } from '../../state/mint/actions'
import { ThemedText } from '../../theme'

@ -35,8 +35,8 @@ import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { unwrappedToken } from 'utils/unwrappedToken'
import { AutoColumn } from '../../components/Column'
import CurrencyLogo from '../../components/CurrencyLogo'
import FormattedCurrencyAmount from '../../components/FormattedCurrencyAmount'
import CurrencyLogo from '../../components/Logo/CurrencyLogo'
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
import { V2_FACTORY_ADDRESSES } from '../../constants/addresses'
import { WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'

@ -11,9 +11,9 @@ import Badge from 'components/Badge'
import { ButtonConfirmed, ButtonGray, ButtonPrimary } from 'components/Button'
import { DarkCard, LightCard } from 'components/Card'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import Loader from 'components/Loader'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { RowBetween, RowFixed } from 'components/Row'
import { Dots } from 'components/swap/styleds'
import Toggle from 'components/Toggle'

@ -13,7 +13,7 @@ import { ButtonDropdownLight } from '../../components/Button'
import { LightCard } from '../../components/Card'
import { BlueCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import CurrencyLogo from '../../components/CurrencyLogo'
import CurrencyLogo from '../../components/Logo/CurrencyLogo'
import { FindPoolTabs } from '../../components/NavigationTabs'
import { MinimalPositionCard } from '../../components/PositionCard'
import Row from '../../components/Row'

@ -9,11 +9,11 @@ import RangeBadge from 'components/Badge/RangeBadge'
import { ButtonConfirmed, ButtonPrimary } from 'components/Button'
import { LightCard } from 'components/Card'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import { Break } from 'components/earn/styled'
import FormattedCurrencyAmount from 'components/FormattedCurrencyAmount'
import Loader from 'components/Loader'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { AddRemoveTabs } from 'components/NavigationTabs'
import { AutoRow, RowBetween, RowFixed } from 'components/Row'
import Slider from 'components/Slider'

@ -18,8 +18,8 @@ import { ButtonConfirmed, ButtonError, ButtonLight, ButtonPrimary } from '../../
import { BlueCard, LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import CurrencyLogo from '../../components/CurrencyLogo'
import DoubleCurrencyLogo from '../../components/DoubleLogo'
import CurrencyLogo from '../../components/Logo/CurrencyLogo'
import { AddRemoveTabs } from '../../components/NavigationTabs'
import { MinimalPositionCard } from '../../components/PositionCard'
import Row, { RowBetween, RowFixed } from '../../components/Row'