feat: include native currency in widget select (#3124)

* fix: token image for chains / natives

* feat: include native currency in select

- Updates widgets swap state to use Currency (and deals with downstream updates)
- Refactors logoURI code to a new lib/hooks/useCurrencyLogoURIs
- Adds native currency to useQueryTokenList

NB: This does not build because tests must be updated to use Currency (they currently use mock tokens)

* test: update fixtures to use real currency

* fix: data uri color extraction

* fix: token img state

* fix: use new array
This commit is contained in:
Zach Pomerantz 2022-01-18 12:11:22 -08:00 committed by GitHub
parent 99f681818f
commit 850a20f6ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 307 additions and 314 deletions

@ -1,14 +1,25 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
/* eslint-disable @typescript-eslint/no-var-requires */
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { DefinePlugin } = require('webpack')
// Renders the cosmos fixtures in isolation, instead of using public/index.html.
module.exports = (webpackConfig) => ({
...webpackConfig,
plugins: webpackConfig.plugins.map((plugin) =>
plugin instanceof HtmlWebpackPlugin
? new HtmlWebpackPlugin({
templateContent: '<body></body>',
})
: plugin
),
plugins: webpackConfig.plugins.map((plugin) => {
if (plugin instanceof HtmlWebpackPlugin) {
return new HtmlWebpackPlugin({
templateContent: '<body></body>',
})
}
if (plugin instanceof DefinePlugin) {
return new DefinePlugin({
...plugin.definitions,
'process.env': {
...plugin.definitions['process.env'],
REACT_APP_IS_WIDGET: true,
},
})
}
return plugin
}),
})

@ -56,6 +56,7 @@
"@types/wcag-contrast": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"@uniswap/default-token-list": "^3.0.0",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1",
@ -94,7 +95,7 @@
"prettier": "^2.2.1",
"qs": "^6.9.4",
"react-confetti": "^6.0.0",
"react-cosmos": "^5.6.3",
"react-cosmos": "^5.6.6",
"react-ga": "^2.5.7",
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
@ -138,7 +139,7 @@
"build": "react-scripts build",
"test": "react-scripts test --env=./custom-test-env.js",
"test:e2e": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run --record'",
"widgets:start": "cross-env FAST_REFRESH=false REACT_APP_IS_WIDGET=true cosmos",
"widgets:start": "cosmos",
"widgets:build": "rollup --config --failAfterWarnings --configPlugin typescript2"
},
"browserslist": {

@ -1,60 +1,18 @@
import { Currency } from '@uniswap/sdk-core'
import EthereumLogo from 'assets/images/ethereum-logo.png'
import MaticLogo from 'assets/svg/matic-token-icon.svg'
import { SupportedChainId } from 'constants/chains'
import useHttpLocations from 'hooks/useHttpLocations'
import React, { useMemo } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import React from 'react'
import styled from 'styled-components/macro'
import Logo from '../Logo'
type Network = 'ethereum' | 'arbitrum' | 'optimism'
function chainIdToNetworkName(networkId: SupportedChainId): Network {
switch (networkId) {
case SupportedChainId.MAINNET:
return 'ethereum'
case SupportedChainId.ARBITRUM_ONE:
return 'arbitrum'
case SupportedChainId.OPTIMISM:
return 'optimism'
default:
return 'ethereum'
}
}
export const getTokenLogoURL = (
address: string,
chainId: SupportedChainId = SupportedChainId.MAINNET
): string | void => {
const networkName = chainIdToNetworkName(chainId)
const networksWithUrls = [SupportedChainId.ARBITRUM_ONE, SupportedChainId.MAINNET, SupportedChainId.OPTIMISM]
if (networksWithUrls.includes(chainId)) {
return `https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/${networkName}/assets/${address}/logo.png`
}
}
const StyledNativeLogo = styled.img<{ 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 white;
-webkit-box-shadow: 0 0 1px white;
box-shadow: 0 0 1px white;
border: 0px solid rgba(255, 255, 255, 0);
`
const StyledLogo = styled(Logo)<{ size: string }>`
const StyledLogo = styled(Logo)<{ size: string; native: boolean }>`
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;
-mox-box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
-webkit-box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
border: 0px solid rgba(255, 255, 255, 0);
`
@ -68,38 +26,16 @@ export default function CurrencyLogo({
size?: string
style?: React.CSSProperties
}) {
const uriLocations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined)
const logoURIs = useCurrencyLogoURIs(currency)
const srcs: string[] = useMemo(() => {
if (!currency || currency.isNative) return []
if (currency.isToken) {
const defaultUrls = []
const url = getTokenLogoURL(currency.address, currency.chainId)
if (url) {
defaultUrls.push(url)
}
if (currency instanceof WrappedTokenInfo) {
return [...uriLocations, ...defaultUrls]
}
return defaultUrls
}
return []
}, [currency, uriLocations])
if (currency?.isNative) {
let nativeLogoUrl: string
switch (currency.chainId) {
case SupportedChainId.POLYGON_MUMBAI:
case SupportedChainId.POLYGON:
nativeLogoUrl = MaticLogo
break
default:
nativeLogoUrl = EthereumLogo
break
}
return <StyledNativeLogo src={nativeLogoUrl} alt="ethereum logo" size={size} style={style} {...rest} />
}
return <StyledLogo size={size} srcs={srcs} alt={`${currency?.symbol ?? 'token'} logo`} style={style} {...rest} />
return (
<StyledLogo
size={size}
native={currency?.isNative ?? false}
srcs={logoURIs}
alt={`${currency?.symbol ?? 'token'} logo`}
style={style}
{...rest}
/>
)
}

@ -1,9 +1,8 @@
import { Currency, Token } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { useCallback, useState } from 'react'
import { getTokenLogoURL } from './../components/CurrencyLogo/index'
export default function useAddTokenToMetamask(currencyToAdd: Currency | undefined): {
addToken: () => void
success: boolean | undefined
@ -13,6 +12,7 @@ export default function useAddTokenToMetamask(currencyToAdd: Currency | undefine
const token: Token | undefined = currencyToAdd?.wrapped
const [success, setSuccess] = useState<boolean | undefined>()
const logoURL = useCurrencyLogoURIs(token)[0]
const addToken = useCallback(() => {
if (library && library.provider.isMetaMask && library.provider.request && token) {
@ -26,7 +26,7 @@ export default function useAddTokenToMetamask(currencyToAdd: Currency | undefine
address: token.address,
symbol: token.symbol,
decimals: token.decimals,
image: getTokenLogoURL(token.address),
image: logoURL,
},
},
})
@ -37,7 +37,7 @@ export default function useAddTokenToMetamask(currencyToAdd: Currency | undefine
} else {
setSuccess(false)
}
}, [library, token])
}, [library, logoURL, token])
return { addToken, success }
}

@ -12,7 +12,6 @@ const Column = styled.div<{
css?: ReturnType<typeof css>
}>`
align-items: ${({ align }) => align ?? 'center'};
background-color: inherit;
color: ${({ color, theme }) => color && theme[color]};
display: ${({ flex }) => (flex ? 'flex' : 'grid')};
flex-direction: column;

@ -1,8 +1,7 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { AlertTriangle, ArrowRight, CheckCircle, Spinner, Trash2 } from 'lib/icons'
import { DAI, ETH, UNI, USDC } from 'lib/mocks'
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { useMemo, useState } from 'react'
import Button from './Button'
@ -13,7 +12,7 @@ import TokenImg from './TokenImg'
interface ITokenAmount {
value: number
token: Token
token: Currency
}
export enum TransactionStatus {
@ -28,25 +27,6 @@ interface ITransaction {
status: TransactionStatus
}
// TODO: integrate with web3-react context
export const mockTxs: ITransaction[] = [
{
input: { value: 4170.15, token: USDC },
output: { value: 4167.44, token: DAI },
status: TransactionStatus.SUCCESS,
},
{
input: { value: 1.23, token: ETH },
output: { value: 4125.02, token: DAI },
status: TransactionStatus.PENDING,
},
{
input: { value: 10, token: UNI },
output: { value: 2125.02, token: ETH },
status: TransactionStatus.ERROR,
},
]
const TransactionRow = styled(Row)`
padding: 0.5em 1em;
@ -94,7 +74,7 @@ function Transaction({ tx }: { tx: ITransaction }) {
}
export default function RecentTransactionsDialog() {
const [txs, setTxs] = useState(mockTxs)
const [txs, setTxs] = useState([])
return (
<>

@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'
import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import useColor, { usePrefetchColor } from 'lib/hooks/useColor'
import useCurrencyColor, { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor'
import { inputAtom, outputAtom, useUpdateInputToken, useUpdateInputValue } from 'lib/state/swap'
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
import { ReactNode, useMemo } from 'react'
@ -40,8 +40,8 @@ export default function Output({ disabled, children }: OutputProps) {
const balance = 123.45
const overrideColor = useAtomValue(colorAtom)
const dynamicColor = useColor(output.token)
usePrefetchColor(input.token) // extract eagerly in case of reversal
const dynamicColor = useCurrencyColor(output.token)
usePrefetchCurrencyColor(input.token) // extract eagerly in case of reversal
const color = overrideColor || dynamicColor
const hasColor = output.token ? Boolean(color) || null : false

@ -1,13 +1,23 @@
import { tokens } from '@uniswap/default-token-list'
import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { DAI, ETH } from 'lib/mocks'
import { transactionAtom } from 'lib/state/swap'
import { useEffect } from 'react'
import { useSelect } from 'react-cosmos/fixture'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import invariant from 'tiny-invariant'
import { Modal } from '../Dialog'
import { StatusDialog } from './Status'
const ETH = nativeOnChain(SupportedChainId.MAINNET)
const UNI = (function () {
const token = tokens.find(({ symbol }) => symbol === 'UNI')
invariant(token)
return new WrappedTokenInfo(token)
})()
function Fixture() {
const setTransaction = useUpdateAtom(transactionAtom)
@ -17,7 +27,7 @@ function Fixture() {
useEffect(() => {
setTransaction({
input: { token: ETH, value: 1 },
output: { token: DAI, value: 4200 },
output: { token: UNI, value: 42 },
receipt: '',
timestamp: Date.now(),
})
@ -27,7 +37,7 @@ function Fixture() {
case 'PENDING':
setTransaction({
input: { token: ETH, value: 1 },
output: { token: DAI, value: 4200 },
output: { token: UNI, value: 42 },
receipt: '',
timestamp: Date.now(),
})

@ -1,12 +1,23 @@
import { tokens } from '@uniswap/default-token-list'
import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { DAI, ETH } from 'lib/mocks'
import { Field, outputAtom, stateAtom } from 'lib/state/swap'
import { useEffect, useState } from 'react'
import { useValue } from 'react-cosmos/fixture'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import invariant from 'tiny-invariant'
import { Modal } from '../Dialog'
import { SummaryDialog } from './Summary'
const ETH = nativeOnChain(SupportedChainId.MAINNET)
const UNI = (function () {
const token = tokens.find(({ symbol }) => symbol === 'UNI')
invariant(token)
return new WrappedTokenInfo(token)
})()
function Fixture() {
const setState = useUpdateAtom(stateAtom)
const [, setInitialized] = useState(false)
@ -15,7 +26,7 @@ function Fixture() {
setState({
activeInput: Field.INPUT,
input: { token: ETH, value: 1, usdc: 4195 },
output: { token: DAI, value: 4200, usdc: 4200 },
output: { token: UNI, value: 42, usdc: 42 },
swap: {
lpFee: 0.0005,
integratorFee: 0.00025,
@ -30,7 +41,7 @@ function Fixture() {
const setOutput = useUpdateAtom(outputAtom)
const [price] = useValue('output value', { defaultValue: 4200 })
useEffect(() => {
setState((state) => ({ ...state, output: { token: DAI, value: price, usdc: price } }))
setState((state) => ({ ...state, output: { token: UNI, value: price, usdc: price } }))
}, [price, setOutput, setState])
return (

@ -1,7 +1,7 @@
import { t } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { State } from 'lib/state/swap'
import { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { useMemo } from 'react'
import Row from '../../Row'
@ -24,8 +24,8 @@ function Detail({ label, value }: DetailProps) {
interface DetailsProps {
swap: Required<State>['swap']
input: Token
output: Token
input: Currency
output: Currency
}
export default function Details({

@ -1,3 +1,4 @@
import { tokens } from '@uniswap/default-token-list'
import { useAtom } from 'jotai'
import { useUpdateAtom } from 'jotai/utils'
import { inputAtom, outputAtom, swapAtom } from 'lib/state/swap'
@ -61,7 +62,7 @@ function Fixture() {
}
}, [color, setColor])
return <Swap />
return <Swap defaults={{ tokenList: tokens }} />
}
export default <Fixture />

@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { Input } from 'lib/state/swap'
import styled, { keyframes, ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react'
import Button from '../Button'
@ -49,7 +49,7 @@ interface TokenInputProps {
disabled?: boolean
onMax?: () => void
onChangeInput: (input: number | undefined) => void
onChangeToken: (token: Token) => void
onChangeToken: (token: Currency) => void
children: ReactNode
}

@ -1,29 +1,31 @@
import useNativeEvent from 'lib/hooks/useNativeEvent'
import { Currency } from '@uniswap/sdk-core'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { Slash } from 'lib/icons'
import styled from 'lib/theme'
import uriToHttp from 'lib/utils/uriToHttp'
import { useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
const badSrcs = new Set<string>()
interface TokenImgProps {
className?: string
token: {
name?: string
symbol: string
logoURI?: string
}
token: Currency
}
const TRANSPARENT_SRC = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
function TokenImg({ className, token }: TokenImgProps) {
const [img, setImg] = useState<HTMLImageElement | null>(null)
const src = token.logoURI ? uriToHttp(token.logoURI)[0] : TRANSPARENT_SRC
useNativeEvent(img, 'error', () => {
if (img) {
// Use a local transparent gif to avoid the browser-dependent broken img icon.
// The icon may still flash, but using a native event further reduces the duration.
img.src = TRANSPARENT_SRC
}
})
return <img className={className} src={src} alt={token.name || token.symbol} ref={setImg} />
const srcs = useCurrencyLogoURIs(token)
const [src, setSrc] = useState<string | undefined>()
useEffect(() => {
setSrc(srcs.find((src) => !badSrcs.has(src)))
}, [srcs])
const onError = useCallback(() => {
if (src) badSrcs.add(src)
setSrc(srcs.find((src) => !badSrcs.has(src)))
}, [src, srcs])
if (src) {
return <img className={className} src={src} alt={token.name || token.symbol} onError={onError} />
}
return <Slash className={className} color="secondary" />
}
export default styled(TokenImg)<{ size?: number }>`

@ -1,5 +1,5 @@
import { Currency } from '@uniswap/sdk-core'
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import Button from '../Button'
import Row from '../Row'
@ -11,8 +11,8 @@ const TokenButton = styled(Button)`
`
interface TokenBaseProps {
value: Token
onClick: (value: Token) => void
value: Currency
onClick: (value: Currency) => void
}
export default function TokenBase({ value, onClick }: TokenBaseProps) {

@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { ChevronDown } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import Button from '../Button'
import Row from '../Row'
@ -27,7 +27,7 @@ const TokenButtonRow = styled(Row)<{ collapsed: boolean }>`
`
interface TokenButtonProps {
value?: Token
value?: Currency
collapsed: boolean
disabled?: boolean
onClick: () => void

@ -1,3 +1,4 @@
import { Currency } from '@uniswap/sdk-core'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import useNativeEvent from 'lib/hooks/useNativeEvent'
@ -18,7 +19,6 @@ import {
} from 'react'
import AutoSizer from 'react-virtualized-auto-sizer'
import { areEqual, FixedSizeList, FixedSizeListProps } from 'react-window'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import invariant from 'tiny-invariant'
import { BaseButton } from '../Button'
@ -33,7 +33,7 @@ const TokenButton = styled(BaseButton)`
`
const ITEM_SIZE = 56
type ItemData = WrappedTokenInfo[]
type ItemData = Currency[]
interface FixedSizeTokenList extends FixedSizeList<ItemData>, ComponentClass<FixedSizeListProps<ItemData>> {}
const TokenList = styled(FixedSizeList as unknown as FixedSizeTokenList)<{
hover: number
@ -57,13 +57,13 @@ const OnHover = styled.div<{ hover: number }>`
interface TokenOptionProps {
index: number
value: WrappedTokenInfo
value: Currency
style: CSSProperties
}
interface BubbledEvent extends SyntheticEvent {
index?: number
token?: WrappedTokenInfo
token?: Currency
ref?: HTMLButtonElement
}
@ -107,7 +107,10 @@ function TokenOption({ index, value, style }: TokenOptionProps) {
)
}
const itemKey = (index: number, tokens: ItemData) => tokens[index]?.address
const itemKey = (index: number, tokens: ItemData) => {
if (tokens[index].isNative) return 'native'
return tokens[index].wrapped.address
}
const ItemRow = memo(function ItemRow({
data: tokens,
index,
@ -127,8 +130,8 @@ interface TokenOptionsHandle {
}
interface TokenOptionsProps {
tokens: WrappedTokenInfo[]
onSelect: (token: WrappedTokenInfo) => void
tokens: Currency[]
onSelect: (token: Currency) => void
}
const TokenOptions = forwardRef<TokenOptionsHandle, TokenOptionsProps>(function TokenOptions(

@ -1,7 +1,7 @@
import { t, Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { useQueryTokenList } from 'lib/hooks/useTokenList'
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { ElementRef, useCallback, useEffect, useRef, useState } from 'react'
import Column from '../Column'
@ -17,11 +17,11 @@ const SearchInput = styled(StringInput)`
${inputCss}
`
export function TokenSelectDialog({ onSelect }: { onSelect: (token: Token) => void }) {
export function TokenSelectDialog({ onSelect }: { onSelect: (token: Currency) => void }) {
const [query, setQuery] = useState('')
const tokens = useQueryTokenList(query)
const baseTokens: Token[] = [] // TODO(zzmp): Add base tokens to token list functionality
const baseTokens: Currency[] = [] // TODO(zzmp): Add base tokens to token list functionality
// TODO(zzmp): Disable already selected tokens (passed as props?)
@ -49,7 +49,7 @@ export function TokenSelectDialog({ onSelect }: { onSelect: (token: Token) => vo
{Boolean(baseTokens.length) && (
<Row pad={0.75} gap={0.25} justify="flex-start" flex>
{baseTokens.map((token) => (
<TokenBase value={token} onClick={onSelect} key={token.address} />
<TokenBase value={token} onClick={onSelect} key={token.wrapped.address} />
))}
</Row>
)}
@ -61,16 +61,16 @@ export function TokenSelectDialog({ onSelect }: { onSelect: (token: Token) => vo
}
interface TokenSelectProps {
value?: Token
value?: Currency
collapsed: boolean
disabled?: boolean
onSelect: (value: Token) => void
onSelect: (value: Currency) => void
}
export default function TokenSelect({ value, collapsed, disabled, onSelect }: TokenSelectProps) {
const [open, setOpen] = useState(false)
const selectAndClose = useCallback(
(value: Token) => {
(value: Currency) => {
onSelect(value)
setOpen(false)
},

@ -1,77 +0,0 @@
import { useTheme } from 'lib/theme'
import { Token } from 'lib/types'
import uriToHttp from 'lib/utils/uriToHttp'
import Vibrant from 'node-vibrant/lib/bundle'
import { useLayoutEffect, useState } from 'react'
const colors = new Map<string, string | undefined>()
function UriForEthToken(address: string) {
return `https://raw.githubusercontent.com/uniswap/assets/master/blockchains/ethereum/assets/${address}/logo.png?color`
}
/**
* Extracts the prominent color from a token.
* NB: If cached, this function returns synchronously; using a callback allows sync or async returns.
*/
async function getColorFromToken(token: Token, cb: (color: string | undefined) => void = () => void 0) {
const { address, chainId, logoURI } = token
const key = chainId + address
let color = colors.get(key)
if (!color && logoURI) {
// Color extraction must use a CORS-compatible resource, but the resource is already cached.
// Add a dummy parameter to force a different browser resource cache entry.
// Without this, color extraction prevents resource caching.
const uri = uriToHttp(logoURI)[0] + '?color'
color = await getColorFromUriPath(uri)
}
if (!color && chainId === 1) {
const fallbackUri = UriForEthToken(address)
color = await getColorFromUriPath(fallbackUri)
}
colors.set(key, color)
return cb(color)
}
async function getColorFromUriPath(uri: string): Promise<string | undefined> {
try {
const palette = await Vibrant.from(uri).getPalette()
return palette.Vibrant?.hex
} catch {}
return
}
export function usePrefetchColor(token?: Token) {
const theme = useTheme()
if (theme.tokenColorExtraction && token) {
getColorFromToken(token)
}
}
export default function useColor(token?: Token) {
const [color, setColor] = useState<string | undefined>(undefined)
const theme = useTheme()
useLayoutEffect(() => {
let stale = false
if (theme.tokenColorExtraction && token) {
getColorFromToken(token, (color) => {
if (!stale && color) {
setColor(color)
}
})
}
return () => {
stale = true
setColor(undefined)
}
}, [token, theme])
return color
}

@ -0,0 +1,78 @@
import { Currency } from '@uniswap/sdk-core'
import { useTheme } from 'lib/theme'
import Vibrant from 'node-vibrant/lib/bundle'
import { useEffect, useLayoutEffect, useState } from 'react'
import useCurrencyLogoURIs from './useCurrencyLogoURIs'
const colors = new Map<string, string | undefined>()
/**
* Extracts the prominent color from a token.
* NB: If cached, this function returns synchronously; using a callback allows sync or async returns.
*/
async function getColorFromLogoURIs(logoURIs: string[], cb: (color: string | undefined) => void = () => void 0) {
const key = logoURIs[0]
let color = colors.get(key)
if (!color) {
for (const logoURI of logoURIs) {
let uri = logoURI
if (logoURI.startsWith('http')) {
// Color extraction must use a CORS-compatible resource, but the resource may already be cached.
// Adds a dummy parameter to force a different browser resource cache entry. Without this, color extraction prevents resource caching.
uri += '?color'
}
color = await getColorFromUriPath(uri)
if (color) break
}
}
colors.set(key, color)
return cb(color)
}
async function getColorFromUriPath(uri: string): Promise<string | undefined> {
try {
const palette = await Vibrant.from(uri).getPalette()
return palette.Vibrant?.hex
} catch {}
return
}
export function usePrefetchCurrencyColor(token?: Currency) {
const theme = useTheme()
const logoURIs = useCurrencyLogoURIs(token)
useEffect(() => {
if (theme.tokenColorExtraction && token) {
getColorFromLogoURIs(logoURIs)
}
}, [token, logoURIs, theme.tokenColorExtraction])
}
export default function useCurrencyColor(token?: Currency) {
const [color, setColor] = useState<string | undefined>(undefined)
const theme = useTheme()
const logoURIs = useCurrencyLogoURIs(token)
useLayoutEffect(() => {
let stale = false
if (theme.tokenColorExtraction && token) {
getColorFromLogoURIs(logoURIs, (color) => {
if (!stale && color) {
setColor(color)
}
})
}
return () => {
stale = true
setColor(undefined)
}
}, [token, logoURIs, theme.tokenColorExtraction])
return color
}

@ -0,0 +1,59 @@
import { Currency } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains'
import useHttpLocations from 'hooks/useHttpLocations'
import { useMemo } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import EthereumLogo from '../../assets/images/ethereum-logo.png'
import MaticLogo from '../../assets/svg/matic-token-icon.svg'
type Network = 'ethereum' | 'arbitrum' | 'optimism'
function chainIdToNetworkName(networkId: SupportedChainId): Network {
switch (networkId) {
case SupportedChainId.MAINNET:
return 'ethereum'
case SupportedChainId.ARBITRUM_ONE:
return 'arbitrum'
case SupportedChainId.OPTIMISM:
return 'optimism'
default:
return 'ethereum'
}
}
function getNativeLogoURI(chainId: SupportedChainId = SupportedChainId.MAINNET): string {
switch (chainId) {
case SupportedChainId.POLYGON_MUMBAI:
case SupportedChainId.POLYGON:
return MaticLogo
default:
return EthereumLogo
}
}
function getTokenLogoURI(address: string, chainId: SupportedChainId = SupportedChainId.MAINNET): string | void {
const networkName = chainIdToNetworkName(chainId)
const networksWithUrls = [SupportedChainId.ARBITRUM_ONE, SupportedChainId.MAINNET, SupportedChainId.OPTIMISM]
if (networksWithUrls.includes(chainId)) {
return `https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/${networkName}/assets/${address}/logo.png`
}
}
export default function useCurrencyLogoURIs(currency?: Currency | null): string[] {
const locations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined)
return useMemo(() => {
const logoURIs = [...locations]
if (currency) {
if (currency.isNative) {
logoURIs.push(getNativeLogoURI(currency.chainId))
} else if (currency.isToken) {
const logoURI = getTokenLogoURI(currency.address, currency.chainId)
if (logoURI) {
logoURIs.push(logoURI)
}
}
}
return logoURIs
}, [currency, locations])
}

@ -1,10 +1,10 @@
import { Currency } from '@uniswap/sdk-core'
import { NativeCurrency } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useMemo } from 'react'
export default function useNativeCurrency(): Currency {
export default function useNativeCurrency(): NativeCurrency {
const { chainId } = useActiveWeb3React()
return useMemo(
() =>

@ -1,4 +1,4 @@
import { Token } from '@uniswap/sdk-core'
import { NativeCurrency, Token } from '@uniswap/sdk-core'
import { TokenInfo } from '@uniswap/token-lists'
import { isAddress } from '../../../utils'
@ -6,12 +6,12 @@ import { isAddress } from '../../../utils'
const alwaysTrue = () => true
/** Creates a filter function that filters tokens that do not match the query. */
export function getTokenFilter<T extends Token | TokenInfo>(query: string): (token: T) => boolean {
export function getTokenFilter<T extends Token | TokenInfo>(query: string): (token: T | NativeCurrency) => boolean {
const searchingAddress = isAddress(query)
if (searchingAddress) {
const lower = searchingAddress.toLowerCase()
return (t: T) => ('isToken' in t ? searchingAddress === t.address : lower === t.address.toLowerCase())
const address = searchingAddress.toLowerCase()
return (t: T | NativeCurrency) => 'address' in t && address === t.address.toLowerCase()
}
const queryParts = query
@ -30,5 +30,5 @@ export function getTokenFilter<T extends Token | TokenInfo>(query: string): (tok
return queryParts.every((p) => p.length === 0 || parts.some((sp) => sp.startsWith(p) || sp.endsWith(p)))
}
return ({ name, symbol }: T): boolean => Boolean((symbol && match(symbol)) || (name && match(name)))
return ({ name, symbol }: T | NativeCurrency): boolean => Boolean((symbol && match(symbol)) || (name && match(name)))
}

@ -1,6 +1,7 @@
import useDebounce from 'hooks/useDebounce'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useTokenBalances } from 'lib/hooks/useCurrencyBalance'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useMemo } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
@ -10,13 +11,23 @@ import { tokenComparator, useSortTokensByQuery } from './sorting'
export function useQueryTokens(query: string, tokens: WrappedTokenInfo[]) {
const { account } = useActiveWeb3React()
const balances = useTokenBalances(account, tokens)
const sortedTokens = useMemo(() => [...tokens.sort(tokenComparator.bind(null, balances))], [balances, tokens])
const debouncedQuery = useDebounce(query, 200)
const filteredTokens = useMemo(
() => sortedTokens.filter(getTokenFilter(debouncedQuery)),
[debouncedQuery, sortedTokens]
const sortedTokens = useMemo(
// Create a new array because sort is in-place and returns a referentially equivalent array.
() => Array.from(tokens).sort(tokenComparator.bind(null, balances)),
[balances, tokens]
)
return useSortTokensByQuery(debouncedQuery, filteredTokens)
const debouncedQuery = useDebounce(query, 200)
const filter = useMemo(() => getTokenFilter(debouncedQuery), [debouncedQuery])
const filteredTokens = useMemo(() => sortedTokens.filter(filter), [filter, sortedTokens])
const queriedTokens = useSortTokensByQuery(debouncedQuery, filteredTokens)
const native = useNativeCurrency()
return useMemo(() => {
if (native && filter(native)) {
return [native, ...queriedTokens]
}
return queriedTokens
}, [filter, native, queriedTokens])
}

@ -14,6 +14,7 @@ import {
HelpCircle as HelpCircleIcon,
Info as InfoIcon,
Settings as SettingsIcon,
Slash as SlashIcon,
Trash2 as Trash2Icon,
X as XIcon,
} from 'react-feather'
@ -77,6 +78,7 @@ export const CreditCard = icon(CreditCardIcon)
export const HelpCircle = icon(HelpCircleIcon)
export const Info = icon(InfoIcon)
export const Settings = icon(SettingsIcon)
export const Slash = icon(SlashIcon)
export const Trash2 = icon(Trash2Icon)
export const X = icon(XIcon)

@ -1,33 +0,0 @@
export const USDC = {
name: 'USDCoin',
symbol: 'USDC',
chainId: 1,
decimals: 18,
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
logoURI:
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
}
export const DAI = {
name: 'DaiStablecoin',
symbol: 'DAI',
chainId: 1,
decimals: 18,
address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
logoURI: 'https://gemini.com/images/currencies/icons/default/dai.svg',
}
export const ETH = {
name: 'Ether',
symbol: 'ETH',
chainId: 1,
decimals: 18,
address: 'ETHER',
logoURI: 'https://raw.githubusercontent.com/Uniswap/interface/main/src/assets/images/ethereum-logo.png',
}
export const UNI = {
name: 'Uniswap',
symbol: 'UNI',
chainId: 1,
decimals: 18,
address: '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984',
logoURI: 'https://gemini.com/images/currencies/icons/default/uni.svg',
}

@ -1,10 +1,11 @@
import { Currency } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens'
import { atom, WritableAtom } from 'jotai'
import { atomWithImmer } from 'jotai/immer'
import { useUpdateAtom } from 'jotai/utils'
import { atomWithReset } from 'jotai/utils'
import { ETH } from 'lib/mocks'
import { Customizable, pickAtom, setCustomizable, setTogglable } from 'lib/state/atoms'
import { Token } from 'lib/types'
import { useMemo } from 'react'
/** Max slippage, as a percentage. */
@ -42,7 +43,7 @@ export enum Field {
export interface Input {
value?: number
token?: Token
token?: Currency
usdc?: number
}
@ -62,7 +63,7 @@ export interface State {
export const stateAtom = atomWithImmer<State>({
activeInput: Field.INPUT,
input: { token: ETH },
input: { token: nativeOnChain(SupportedChainId.MAINNET) },
output: {},
})

8
src/lib/types.d.ts vendored

@ -1,8 +0,0 @@
export interface Token {
name: string
symbol: string
chainId: number
decimals: number
address: string
logoURI?: string
}

@ -13,6 +13,7 @@
"hooks/*": ["../hooks/*"],
"state/*": ["../state/*"],
"types/*": ["../types/*"],
"utils/*": ["../utils/*"],
},
},
"exclude": ["node_modules", "src/lib/**/*.test.*"],

@ -5069,6 +5069,11 @@
resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-2.2.0.tgz#d85a5c2520f57f4920bd989dfc9f01e1b701a567"
integrity sha512-vFPWoGzDjHP4i2l7yLaober/lZMmzOZXXirVF8XNyfNzRxgmYCWKO6SzKtfEUwxpd3/KUebgdK55II4Mnak62A==
"@uniswap/default-token-list@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-3.0.0.tgz#427ff2a65bbc77a5a24e60f6158441956773f684"
integrity sha512-t0H96s1Mx2ga6cGMj/wP/3NWdX4c9yZFd0ydiTOWLhWf1i5RjhWWND/ZTdn8QhmNsdHlhrGsWrEU62xoAY11bw==
"@uniswap/governance@^1.0.2":
version "1.0.2"
resolved "https://registry.npmjs.org/@uniswap/governance/-/governance-1.0.2.tgz"
@ -17498,10 +17503,10 @@ react-cosmos-shared2@^5.6.3:
react-is "^17.0.2"
socket.io-client "2.2.0"
react-cosmos@^5.6.3:
version "5.6.3"
resolved "https://registry.yarnpkg.com/react-cosmos/-/react-cosmos-5.6.3.tgz#bd2c0e1334b2c9992ddb3a5d8dfcbe5fc723cbc8"
integrity sha512-FG6VIc4prnqnNQ9ToBq2cNPkPxZcZRauL0tMkbi01aoS90c3sNoQt6koL/IsntSpPcR67KRe+OtJspq9OtBjxg==
react-cosmos@^5.6.6:
version "5.6.6"
resolved "https://registry.yarnpkg.com/react-cosmos/-/react-cosmos-5.6.6.tgz#93d66e347a63da7dfe046c2cb23221dcf815ce9d"
integrity sha512-RMLRjl2gFq9370N6QszjPRMaT5WsEBEkJBsFbz56h00xPnJAxsab8gu5yj6yDDDSFibL/jBgxjJLdqbF00HMNw==
dependencies:
"@skidding/launch-editor" "^2.2.3"
"@skidding/webpack-hot-middleware" "^2.25.0"
@ -17520,7 +17525,7 @@ react-cosmos@^5.6.3:
react-cosmos-playground2 "^5.6.3"
react-cosmos-plugin "^5.6.0"
react-cosmos-shared2 "^5.6.3"
react-error-overlay "^6.0.9"
react-error-overlay "6.0.9"
regenerator-runtime "^0.13.7"
resolve-from "^5.0.0"
slash "^3.0.0"
@ -17584,7 +17589,7 @@ react-error-boundary@^3.1.0:
dependencies:
"@babel/runtime" "^7.12.5"
react-error-overlay@^6.0.9:
react-error-overlay@6.0.9, react-error-overlay@^6.0.9:
version "6.0.9"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==