Commit bbe42b81 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

fix: display token images on first pageload (#6956)

* fix: always show img for common bases

* fix: include backup img in first render

* fix: initialize and update token safety lookup

* test: update snapshots to include initial logos

* refactor: better code colocation

* test: updating token safety lookup table

* refactor: tokenSafetyLookup

* refactor: tokenLogoLookup

* fix: pass lists state to token safety update

* test: mock initial update
parent e9f469d3
......@@ -80,7 +80,7 @@ exports[`PortfolioLogo renders with L2 icon 1`] = `
>
<img
class="c2 c3"
src="blank_token.svg"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1/logo.png"
/>
<img
class="c2 c3"
......@@ -152,11 +152,11 @@ exports[`PortfolioLogo renders without L2 icon 1`] = `
>
<img
class="c2 c3"
src="blank_token.svg"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png"
/>
<img
class="c2 c3"
src="blank_token.svg"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
/>
</div>
</div>
......
......@@ -19,16 +19,11 @@ exports[`LoadedRow.tsx renders a row 1`] = `
}
.c8 {
--size: 24px;
border-radius: 100px;
color: #0D111C;
background-color: #E8ECFB;
font-size: calc(var(--size) / 3);
font-weight: 500;
height: 24px;
line-height: 24px;
text-align: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px));
box-shadow: 0 0 1px white;
}
.c7 {
......@@ -351,11 +346,11 @@ exports[`LoadedRow.tsx renders a row 1`] = `
<div
class="c7"
>
<div
<img
alt="USDC logo"
class="c8"
>
USD
</div>
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
/>
<div
class="c9"
/>
......
......@@ -252,7 +252,7 @@ exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX s
color: #0D111C;
}
.c12 {
.c13 {
width: 100%;
height: 1px;
border-width: 0;
......@@ -296,7 +296,7 @@ exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX s
grid-row-gap: 8px;
}
.c11 {
.c12 {
--size: 36px;
border-radius: 100px;
color: #0D111C;
......@@ -309,6 +309,14 @@ exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX s
width: 36px;
}
.c11 {
width: 36px;
height: 36px;
border-radius: 50%;
background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px));
box-shadow: 0 0 1px white;
}
.c10 {
position: relative;
display: -webkit-box;
......@@ -328,7 +336,7 @@ exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX s
margin-right: 8px;
}
.c13 {
.c14 {
margin: 16px 2px 24px 2px;
}
......@@ -378,11 +386,11 @@ exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX s
<div
class="c10"
>
<div
<img
alt="ETH logo"
class="c11"
>
ETH
</div>
src="ethereum-logo.png"
/>
</div>
</div>
<div
......@@ -422,7 +430,7 @@ exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX s
class="c10"
>
<div
class="c11"
class="c12"
>
DEF
</div>
......@@ -430,7 +438,7 @@ exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX s
</div>
</div>
<div
class="c12 c13"
class="c13 c14"
/>
</div>
</DocumentFragment>
......
......@@ -4,7 +4,7 @@ import { SearchToken } from 'graphql/data/SearchTokens'
import { ZERO_ADDRESS } from './misc'
import { NATIVE_CHAIN_ID } from './tokens'
import WarningCache, { TOKEN_LIST_TYPES } from './TokenSafetyLookupTable'
import tokenSafetyLookup, { TOKEN_LIST_TYPES } from './tokenSafetyLookup'
export const TOKEN_SAFETY_ARTICLE = 'https://support.uniswap.org/hc/en-us/articles/8723118437133'
......@@ -84,11 +84,11 @@ export const NotFoundWarning: Warning = {
canProceed: false,
}
export function checkWarning(tokenAddress: string) {
export function checkWarning(tokenAddress: string, chainId?: number | null) {
if (tokenAddress === NATIVE_CHAIN_ID || tokenAddress === ZERO_ADDRESS) {
return null
}
switch (WarningCache.checkToken(tokenAddress.toLowerCase())) {
switch (tokenSafetyLookup.checkToken(tokenAddress.toLowerCase(), chainId)) {
case TOKEN_LIST_TYPES.UNI_DEFAULT:
return null
case TOKEN_LIST_TYPES.UNI_EXTENDED:
......
import { TokenInfo } from '@uniswap/token-lists'
import { ListsState } from 'state/lists/reducer'
import store from '../state'
import { UNI_EXTENDED_LIST, UNI_LIST, UNSUPPORTED_LIST_URLS } from './lists'
import { COMMON_BASES } from './routing'
import brokenTokenList from './tokenLists/broken.tokenlist.json'
import { NATIVE_CHAIN_ID } from './tokens'
......@@ -14,44 +16,47 @@ export enum TOKEN_LIST_TYPES {
}
class TokenSafetyLookupTable {
dict: { [key: string]: TOKEN_LIST_TYPES } | null = null
initialized = false
dict: { [key: string]: TOKEN_LIST_TYPES } = {}
createMap() {
const dict: { [key: string]: TOKEN_LIST_TYPES } = {}
// TODO(WEB-2488): Index lookups by chainId
update(lists: ListsState) {
this.initialized = true
// Initialize extended tokens first
store.getState().lists.byUrl[UNI_EXTENDED_LIST].current?.tokens.forEach((token) => {
dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.UNI_EXTENDED
lists.byUrl[UNI_EXTENDED_LIST].current?.tokens.forEach((token) => {
this.dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.UNI_EXTENDED
})
// Initialize default tokens second, so that any tokens on both default and extended will display as default (no warning)
store.getState().lists.byUrl[UNI_LIST].current?.tokens.forEach((token) => {
dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.UNI_DEFAULT
lists.byUrl[UNI_LIST].current?.tokens.forEach((token) => {
this.dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.UNI_DEFAULT
})
// TODO: Figure out if this list is still relevant
brokenTokenList.tokens.forEach((token) => {
dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.BROKEN
this.dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.BROKEN
})
// Initialize blocked tokens from all urls included
UNSUPPORTED_LIST_URLS.map((url) => store.getState().lists.byUrl[url].current?.tokens)
UNSUPPORTED_LIST_URLS.map((url) => lists.byUrl[url].current?.tokens)
.filter((x): x is TokenInfo[] => !!x)
.flat(1)
.forEach((token) => {
dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.BLOCKED
this.dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.BLOCKED
})
return dict
}
checkToken(address: string) {
if (!this.dict) {
this.dict = this.createMap()
}
checkToken(address: string, chainId?: number | null) {
if (!this.initialized) this.update(store.getState().lists)
if (address === NATIVE_CHAIN_ID.toLowerCase()) {
return TOKEN_LIST_TYPES.UNI_DEFAULT
} else if (chainId && COMMON_BASES[chainId]?.some((base) => address === base.wrapped.address.toLowerCase())) {
return TOKEN_LIST_TYPES.UNI_DEFAULT
} else {
return this.dict[address] ?? TOKEN_LIST_TYPES.UNKNOWN
}
return this.dict[address] ?? TOKEN_LIST_TYPES.UNKNOWN
}
}
......
import TokenLogoLookupTable from 'constants/TokenLogoLookupTable'
import tokenLogoLookup from 'constants/tokenLogoLookup'
import { isCelo, nativeOnChain } from 'constants/tokens'
import { chainIdToNetworkName, getNativeLogoURI } from 'lib/hooks/useCurrencyLogoURIs'
import uriToHttp from 'lib/utils/uriToHttp'
......@@ -38,7 +38,12 @@ function prioritizeLogoSources(uris: string[]) {
return coingeckoUrl ? [...preferredUris, coingeckoUrl] : preferredUris
}
function getInitialUrl(address?: string | null, chainId?: number | null, isNative?: boolean) {
function getInitialUrl(
address?: string | null,
chainId?: number | null,
isNative?: boolean,
backupImg?: string | null
) {
if (chainId && isNative) return getNativeLogoURI(chainId)
const networkName = chainId ? chainIdToNetworkName(chainId) : 'ethereum'
......@@ -51,7 +56,7 @@ function getInitialUrl(address?: string | null, chainId?: number | null, isNativ
if (checksummedAddress) {
return `https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/${networkName}/assets/${checksummedAddress}/logo.png`
} else {
return undefined
return backupImg ?? undefined
}
}
......@@ -61,17 +66,17 @@ export default function useAssetLogoSource(
isNative?: boolean,
backupImg?: string | null
): [string | undefined, () => void] {
const hasWarning = Boolean(address && checkWarning(address))
const showLogo = Boolean((address && checkWarning(address, chainId) === null) || isNative)
const [current, setCurrent] = useState<string | undefined>(
hasWarning ? undefined : getInitialUrl(address, chainId, isNative)
showLogo ? getInitialUrl(address, chainId, isNative, backupImg) : undefined
)
const [fallbackSrcs, setFallbackSrcs] = useState<string[] | undefined>(undefined)
useEffect(() => {
if (hasWarning) return
if (!showLogo) return
setCurrent(getInitialUrl(address, chainId, isNative))
setFallbackSrcs(undefined)
}, [hasWarning, address, chainId, isNative])
}, [address, chainId, isNative, showLogo])
const nextSrc = useCallback(() => {
if (current) {
......@@ -79,7 +84,7 @@ export default function useAssetLogoSource(
}
// Parses and stores logo sources from tokenlists if assets repo url fails
if (!fallbackSrcs) {
const uris = TokenLogoLookupTable.getIcons(address, chainId) ?? []
const uris = tokenLogoLookup.getIcons(address, chainId) ?? []
if (backupImg) uris.push(backupImg)
const tokenListIcons = prioritizeLogoSources(parseLogoSources(uris))
......
......@@ -698,16 +698,11 @@ exports[`disable nft on landing page does not render nft information and card 1`
}
.c29 {
--size: 24px;
border-radius: 100px;
color: #0D111C;
background-color: #E8ECFB;
font-size: calc(var(--size) / 3);
font-weight: 500;
height: 24px;
line-height: 24px;
text-align: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px));
box-shadow: 0 0 1px white;
}
.c28 {
......@@ -1779,11 +1774,11 @@ exports[`disable nft on landing page does not render nft information and card 1`
class="c28"
style="margin-right: 2px;"
>
<div
<img
alt="ETH logo"
class="c29"
>
ETH
</div>
src="ethereum-logo.png"
/>
</div>
<span
class="c30 token-symbol-container"
......@@ -3193,16 +3188,11 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
}
.c29 {
--size: 24px;
border-radius: 100px;
color: #0D111C;
background-color: #E8ECFB;
font-size: calc(var(--size) / 3);
font-weight: 500;
height: 24px;
line-height: 24px;
text-align: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px));
box-shadow: 0 0 1px white;
}
.c28 {
......@@ -4286,11 +4276,11 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
class="c28"
style="margin-right: 2px;"
>
<div
<img
alt="ETH logo"
class="c29"
>
ETH
</div>
src="ethereum-logo.png"
/>
</div>
<span
class="c30 token-symbol-container"
......
import { createStore, Store } from 'redux'
import { DEFAULT_LIST_OF_LISTS } from '../../constants/lists'
import tokenSafetyLookup from '../../constants/tokenSafetyLookup'
import { updateVersion } from '../global/actions'
import { acceptListUpdate, addList, fetchTokenList, removeList } from './actions'
import reducer, { ListsState } from './reducer'
......@@ -79,10 +80,15 @@ describe('list reducer', () => {
})
describe('fulfilled', () => {
beforeEach(() => {
jest.spyOn(tokenSafetyLookup, 'update').mockReturnValue(undefined)
})
it('saves the list', () => {
store.dispatch(
fetchTokenList.fulfilled({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
)
expect(tokenSafetyLookup.update).toHaveBeenCalled()
expect(store.getState()).toEqual({
byUrl: {
'fake-url': {
......@@ -100,9 +106,11 @@ describe('list reducer', () => {
store.dispatch(
fetchTokenList.fulfilled({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
)
expect(tokenSafetyLookup.update).toHaveBeenCalled()
store.dispatch(
fetchTokenList.fulfilled({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
)
expect(tokenSafetyLookup.update).toHaveBeenCalledTimes(1) // should not be called again
expect(store.getState()).toEqual({
byUrl: {
'fake-url': {
......@@ -120,10 +128,11 @@ describe('list reducer', () => {
store.dispatch(
fetchTokenList.fulfilled({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
)
expect(tokenSafetyLookup.update).toHaveBeenCalled()
store.dispatch(
fetchTokenList.fulfilled({ tokenList: PATCHED_STUB_LIST, requestId: 'request-id', url: 'fake-url' })
)
expect(tokenSafetyLookup.update).toHaveBeenCalledTimes(1) // should not be called again
expect(store.getState()).toEqual({
byUrl: {
'fake-url': {
......@@ -140,10 +149,11 @@ describe('list reducer', () => {
store.dispatch(
fetchTokenList.fulfilled({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
)
expect(tokenSafetyLookup.update).toHaveBeenCalled()
store.dispatch(
fetchTokenList.fulfilled({ tokenList: MINOR_UPDATED_STUB_LIST, requestId: 'request-id', url: 'fake-url' })
)
expect(tokenSafetyLookup.update).toHaveBeenCalledTimes(1) // should not be called again
expect(store.getState()).toEqual({
byUrl: {
'fake-url': {
......@@ -160,10 +170,11 @@ describe('list reducer', () => {
store.dispatch(
fetchTokenList.fulfilled({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
)
expect(tokenSafetyLookup.update).toHaveBeenCalled()
store.dispatch(
fetchTokenList.fulfilled({ tokenList: MAJOR_UPDATED_STUB_LIST, requestId: 'request-id', url: 'fake-url' })
)
expect(tokenSafetyLookup.update).toHaveBeenCalledTimes(1) // should not be called again
expect(store.getState()).toEqual({
byUrl: {
'fake-url': {
......
import { createReducer } from '@reduxjs/toolkit'
import { getVersionUpgrade, TokenList, VersionUpgrade } from '@uniswap/token-lists'
import tokenSafetyLookup from 'constants/tokenSafetyLookup'
import { DEFAULT_LIST_OF_LISTS } from '../../constants/lists'
import { updateVersion } from '../global/actions'
......@@ -76,6 +77,7 @@ export default createReducer(initialState, (builder) =>
loadingRequestId: null,
error: null,
}
tokenSafetyLookup.update(state)
}
})
.addCase(fetchTokenList.rejected, (state, { payload: { url, requestId, errorMessage } }) => {
......
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