feat(token lists): implement the uniswap default list as a token list (#983)
* load tokens from url `useTokenList` * improve performance of the loading * move the loading to redux and save loaded lists * lint error * move the list fetching code to a separate component * change how token lists are fetched to use the updater and add unit tests * fix a crash with currencyEquals * bump sdk version * token lists should automatically update for minor/patch changes * nit * show popups for list updates * support pointing at localhost * spuport ipfs/ipns logos * use the updater to bump list versions * save the old/new list in the popup for viewing diffs * improve the list popup * fix linter error, make sure visibility checking is working * show list update notifications * address a couple metamask warnings, linter error * fix the custom added/default tokens * refactor some popup stuff to reuse the fader * linter error * Revert: refactor some popup stuff to reuse the fader (a7b0f752) * style improvements, linter * add to the readme, drop the token-request template * back to the beta that works with wallet connect * get the dependencies to a state that works with wallet connect and passes integration tests
This commit is contained in:
parent
32d300009e
commit
365b429c0b
27
.github/ISSUE_TEMPLATE/token-request.md
vendored
27
.github/ISSUE_TEMPLATE/token-request.md
vendored
@ -1,27 +0,0 @@
|
||||
---
|
||||
name: Token Request
|
||||
about: Request a token addition
|
||||
title: ''
|
||||
labels: token request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Please provide the following information for your token.**
|
||||
|
||||
Token Address:
|
||||
Token Name (from contract):
|
||||
Token Decimals (from contract):
|
||||
Token Symbol (from contract):
|
||||
Uniswap Exchange Address of Token:
|
||||
|
||||
Link to the official homepage of token:
|
||||
Link to CoinMarketCap or CoinGecko page of token:
|
||||
|
||||
Some tokens (e.g. BNB) do not work with Uniswap v1. In order to assess if your token works correctly, please complete small-value transactions of each of the types below, and submit the Etherscan transaction links for our review.
|
||||
Test `addLiquidity` transaction:
|
||||
Test `swap` transaction:
|
||||
Test `removeLiquidity` transaction:
|
||||
|
||||
Are you willing to add liquidity to the liquidity pool for this token? (Y/N):
|
||||
If so, how much liquidity are you willing to add?:
|
@ -20,6 +20,12 @@ To access the Uniswap Interface, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
|
||||
or visit [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
## Listing a token
|
||||
|
||||
Please see the
|
||||
[@uniswap/default-token-list](https://github.com/uniswap/default-token-list)
|
||||
repository.
|
||||
|
||||
## Development
|
||||
|
||||
### Install Dependencies
|
||||
|
24
package.json
24
package.json
@ -4,15 +4,17 @@
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@ethersproject/address": "^5.0.1",
|
||||
"@ethersproject/bignumber": "^5.0.3",
|
||||
"@ethersproject/constants": "^5.0.1",
|
||||
"@ethersproject/contracts": "^5.0.1",
|
||||
"@ethersproject/experimental": "^5.0.0",
|
||||
"@ethersproject/providers": "^5.0.4",
|
||||
"@ethersproject/strings": "^5.0.1",
|
||||
"@ethersproject/units": "^5.0.1",
|
||||
"@ethersproject/wallet": "^5.0.1",
|
||||
"@ethersproject/address": "5.0.0-beta.134",
|
||||
"@ethersproject/bignumber": "5.0.0-beta.138",
|
||||
"@ethersproject/constants": "5.0.0-beta.133",
|
||||
"@ethersproject/contracts": "5.0.0-beta.151",
|
||||
"@ethersproject/experimental": "5.0.0-beta.141",
|
||||
"@ethersproject/networks": "5.0.0-beta.136",
|
||||
"@ethersproject/providers": "5.0.0-beta.162",
|
||||
"@ethersproject/solidity": "5.0.2",
|
||||
"@ethersproject/strings": "5.0.0-beta.136",
|
||||
"@ethersproject/units": "5.0.0-beta.132",
|
||||
"@ethersproject/wallet": "5.0.0-beta.141",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
@ -31,7 +33,8 @@
|
||||
"@types/testing-library__cypress": "^5.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^2.31.0",
|
||||
"@uniswap/sdk": "3.0.1",
|
||||
"@uniswap/sdk": "3.0.3-beta.1",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.9",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@web3-react/core": "^6.0.9",
|
||||
@ -40,6 +43,7 @@
|
||||
"@web3-react/portis-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.1.1",
|
||||
"@web3-react/walletlink-connector": "^6.0.9",
|
||||
"ajv": "^6.12.3",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"cypress": "^4.5.0",
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Currency, Token } from '@uniswap/sdk'
|
||||
|
||||
import EthereumLogo from '../../assets/images/ethereum-logo.png'
|
||||
import { WrappedTokenInfo } from '../../state/lists/hooks'
|
||||
import uriToHttp from '../../utils/uriToHttp'
|
||||
|
||||
const getTokenLogoURL = address =>
|
||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||
const NO_LOGO_ADDRESSES: { [tokenAddress: string]: true } = {}
|
||||
const BAD_URIS: { [tokenAddress: string]: true } = {}
|
||||
|
||||
const Image = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
@ -44,35 +46,49 @@ export default function CurrencyLogo({
|
||||
}) {
|
||||
const [, refresh] = useState<number>(0)
|
||||
|
||||
if (currency instanceof Token) {
|
||||
let path = ''
|
||||
if (!NO_LOGO_ADDRESSES[currency.address]) {
|
||||
path = getTokenLogoURL(currency.address)
|
||||
} else {
|
||||
return (
|
||||
<Emoji {...rest} size={size}>
|
||||
<span role="img" aria-label="Thinking">
|
||||
🤔
|
||||
</span>
|
||||
</Emoji>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
{...rest}
|
||||
alt={`${currency.name} Logo`}
|
||||
src={path}
|
||||
size={size}
|
||||
onError={() => {
|
||||
if (currency instanceof Token) {
|
||||
NO_LOGO_ADDRESSES[currency.address] = true
|
||||
}
|
||||
refresh(i => i + 1)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
if (currency === ETHER) {
|
||||
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
|
||||
}
|
||||
|
||||
if (currency instanceof Token) {
|
||||
let uri: string | undefined
|
||||
|
||||
if (currency instanceof WrappedTokenInfo) {
|
||||
if (currency.logoURI && !BAD_URIS[currency.logoURI]) {
|
||||
uri = uriToHttp(currency.logoURI).filter(s => !BAD_URIS[s])[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (!uri) {
|
||||
const defaultUri = getTokenLogoURL(currency.address)
|
||||
if (!BAD_URIS[defaultUri]) {
|
||||
uri = defaultUri
|
||||
}
|
||||
}
|
||||
|
||||
if (uri) {
|
||||
return (
|
||||
<Image
|
||||
{...rest}
|
||||
alt={`${currency.name} Logo`}
|
||||
src={uri}
|
||||
size={size}
|
||||
onError={() => {
|
||||
if (currency instanceof Token) {
|
||||
BAD_URIS[uri] = true
|
||||
}
|
||||
refresh(i => i + 1)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Emoji {...rest} size={size}>
|
||||
<span role="img" aria-label="Thinking">
|
||||
🤔
|
||||
</span>
|
||||
</Emoji>
|
||||
)
|
||||
}
|
||||
|
72
src/components/Popups/ListUpdatePopup.tsx
Normal file
72
src/components/Popups/ListUpdatePopup.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { TokenList, Version } from '@uniswap/token-lists'
|
||||
import React, { useCallback, useContext } from 'react'
|
||||
import { AlertCircle, Info } from 'react-feather'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { AppDispatch } from '../../state'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { acceptListUpdate } from '../../state/lists/actions'
|
||||
import { TYPE } from '../../theme'
|
||||
import { ButtonPrimary, ButtonSecondary } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
function versionLabel(version: Version): string {
|
||||
return `v${version.major}.${version.minor}.${version.patch}`
|
||||
}
|
||||
|
||||
export default function ListUpdatePopup({
|
||||
popKey,
|
||||
listUrl,
|
||||
oldList,
|
||||
newList,
|
||||
auto
|
||||
}: {
|
||||
popKey: string
|
||||
listUrl: string
|
||||
oldList: TokenList
|
||||
newList: TokenList
|
||||
auto: boolean
|
||||
}) {
|
||||
const removePopup = useRemovePopup()
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const updateList = useCallback(() => {
|
||||
if (auto) return
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
removeThisPopup()
|
||||
}, [auto, dispatch, listUrl, removeThisPopup])
|
||||
|
||||
return (
|
||||
<AutoRow>
|
||||
<div style={{ paddingRight: 16 }}>
|
||||
{auto ? <Info color={theme.text2} size={24} /> : <AlertCircle color={theme.red1} size={24} />}{' '}
|
||||
</div>
|
||||
<AutoColumn style={{ flex: '1' }} gap="8px">
|
||||
{auto ? (
|
||||
<TYPE.body fontWeight={500}>
|
||||
The token list "{oldList.name}" has been updated to{' '}
|
||||
<strong>{versionLabel(newList.version)}</strong>.
|
||||
</TYPE.body>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
A token list update is available for the list "{oldList.name}" ({versionLabel(oldList.version)}{' '}
|
||||
to {versionLabel(newList.version)}).
|
||||
</div>
|
||||
<AutoRow>
|
||||
<div style={{ flexGrow: 1, marginRight: 6 }}>
|
||||
<ButtonPrimary onClick={updateList}>Update list</ButtonPrimary>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<ButtonSecondary onClick={removeThisPopup}>Dismiss</ButtonSecondary>
|
||||
</div>
|
||||
</AutoRow>
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
)
|
||||
}
|
86
src/components/Popups/PopupItem.tsx
Normal file
86
src/components/Popups/PopupItem.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import React, { useCallback, useContext, useState } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import { PopupContent } from '../../state/application/actions'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import ListUpdatePopup from './ListUpdatePopup'
|
||||
import TxnPopup from './TxnPopup'
|
||||
|
||||
export const StyledClose = styled(X)`
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
export const Popup = styled.div`
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
padding-right: 35px;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
min-width: 290px;
|
||||
`}
|
||||
`
|
||||
const DELAY = 100
|
||||
const Fader = styled.div<{ count: number }>`
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: ${({ count }) => `calc(100% - (100% / ${150 / count}))`};
|
||||
height: 2px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
transition: width 100ms linear;
|
||||
`
|
||||
|
||||
export default function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
|
||||
const [count, setCount] = useState(1)
|
||||
|
||||
const [isRunning, setIsRunning] = useState(true)
|
||||
const removePopup = useRemovePopup()
|
||||
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
count > 150 ? removeThisPopup() : setCount(count + 1)
|
||||
},
|
||||
isRunning ? DELAY : null
|
||||
)
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
|
||||
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
|
||||
|
||||
let popupContent
|
||||
if ('txn' in content) {
|
||||
const {
|
||||
txn: { hash, success, summary }
|
||||
} = content
|
||||
popupContent = <TxnPopup hash={hash} success={success} summary={summary} />
|
||||
} else if ('listUpdate' in content) {
|
||||
const {
|
||||
listUpdate: { listUrl, oldList, newList, auto }
|
||||
} = content
|
||||
popupContent = <ListUpdatePopup popKey={popKey} listUrl={listUrl} oldList={oldList} newList={newList} auto={auto} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<StyledClose color={theme.text2} onClick={() => removePopup(popKey)} />
|
||||
{popupContent}
|
||||
<Fader count={count} />
|
||||
</Popup>
|
||||
)
|
||||
}
|
27
src/components/Popups/TxnPopup.tsx
Normal file
27
src/components/Popups/TxnPopup.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { AlertCircle, CheckCircle } from 'react-feather'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
export default function TxnPopup({ hash, success, summary }: { hash: string; success?: boolean; summary?: string }) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<AutoRow>
|
||||
<div style={{ paddingRight: 16 }}>
|
||||
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
|
||||
</div>
|
||||
<AutoColumn gap="8px">
|
||||
<TYPE.body fontWeight={500}>{summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
)
|
||||
}
|
@ -1,22 +1,9 @@
|
||||
import React, { useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useMediaLayout } from 'use-media'
|
||||
|
||||
import { X } from 'react-feather'
|
||||
import { PopupContent } from '../../state/application/actions'
|
||||
import { useActivePopups, useRemovePopup } from '../../state/application/hooks'
|
||||
import { useActivePopups } from '../../state/application/hooks'
|
||||
import { AutoColumn } from '../Column'
|
||||
import TxnPopup from '../TxnPopup'
|
||||
|
||||
const StyledClose = styled(X)`
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
import PopupItem from './PopupItem'
|
||||
|
||||
const MobilePopupWrapper = styled.div<{ height: string | number }>`
|
||||
position: relative;
|
||||
@ -50,37 +37,9 @@ const FixedPopupColumn = styled(AutoColumn)`
|
||||
`};
|
||||
`
|
||||
|
||||
const Popup = styled.div`
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
padding-right: 35px;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
min-width: 290px;
|
||||
`}
|
||||
`
|
||||
|
||||
function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
|
||||
if ('txn' in content) {
|
||||
const {
|
||||
txn: { hash, success, summary }
|
||||
} = content
|
||||
return <TxnPopup popKey={popKey} hash={hash} success={success} summary={summary} />
|
||||
}
|
||||
}
|
||||
|
||||
export default function Popups() {
|
||||
const theme = useContext(ThemeContext)
|
||||
// get all popups
|
||||
const activePopups = useActivePopups()
|
||||
const removePopup = useRemovePopup()
|
||||
|
||||
// switch view settings on mobile
|
||||
const isMobile = useMediaLayout({ maxWidth: '600px' })
|
||||
@ -88,14 +47,9 @@ export default function Popups() {
|
||||
if (!isMobile) {
|
||||
return (
|
||||
<FixedPopupColumn gap="20px">
|
||||
{activePopups.map(item => {
|
||||
return (
|
||||
<Popup key={item.key}>
|
||||
<StyledClose color={theme.text2} onClick={() => removePopup(item.key)} />
|
||||
<PopupItem content={item.content} popKey={item.key} />
|
||||
</Popup>
|
||||
)
|
||||
})}
|
||||
{activePopups.map(item => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} />
|
||||
))}
|
||||
</FixedPopupColumn>
|
||||
)
|
||||
}
|
||||
@ -107,14 +61,9 @@ export default function Popups() {
|
||||
{activePopups // reverse so new items up front
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map(item => {
|
||||
return (
|
||||
<Popup key={item.key}>
|
||||
<StyledClose color={theme.text2} onClick={() => removePopup(item.key)} />
|
||||
<PopupItem content={item.content} popKey={item.key} />
|
||||
</Popup>
|
||||
)
|
||||
})}
|
||||
.map(item => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} />
|
||||
))}
|
||||
</MobilePopupInner>
|
||||
</MobilePopupWrapper>
|
||||
)
|
||||
|
@ -30,7 +30,7 @@ export default function CommonBases({
|
||||
onSelect,
|
||||
selectedCurrency
|
||||
}: {
|
||||
chainId: ChainId
|
||||
chainId?: ChainId
|
||||
selectedCurrency?: Currency
|
||||
onSelect: (currency: Currency) => void
|
||||
}) {
|
||||
@ -52,8 +52,8 @@ export default function CommonBases({
|
||||
ETH
|
||||
</Text>
|
||||
</BaseWrapper>
|
||||
{(SUGGESTED_BASES[chainId as ChainId] ?? []).map((token: Token) => {
|
||||
const selected = currencyEquals(selectedCurrency, token)
|
||||
{(chainId ? SUGGESTED_BASES[chainId] : []).map((token: Token) => {
|
||||
const selected = selectedCurrency instanceof Token && selectedCurrency.address === token.address
|
||||
return (
|
||||
<BaseWrapper onClick={() => !selected && onSelect(token)} disable={selected} key={token.address}>
|
||||
<CurrencyLogo currency={token} style={{ marginRight: 8 }} />
|
||||
|
@ -5,6 +5,7 @@ import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useDefaultTokenList } from '../../state/lists/hooks'
|
||||
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { useETHBalances } from '../../state/wallet/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
@ -14,7 +15,7 @@ import { RowFixed } from '../Row'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { FadedSpan, MenuItem } from './styleds'
|
||||
import Loader from '../Loader'
|
||||
import { isDefaultToken, isCustomAddedToken } from '../../utils'
|
||||
import { isDefaultToken } from '../../utils'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : ''
|
||||
@ -38,6 +39,7 @@ export default function CurrencyList({
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
const allTokens = useAllTokens()
|
||||
const defaultTokens = useDefaultTokenList()
|
||||
const addToken = useAddUserToken()
|
||||
const removeToken = useRemoveUserAddedToken()
|
||||
const ETHBalance = useETHBalances([account])[account]
|
||||
@ -46,8 +48,8 @@ export default function CurrencyList({
|
||||
return memo(function CurrencyRow({ index, style }: { index: number; style: CSSProperties }) {
|
||||
const currency = index === 0 ? Currency.ETHER : currencies[index - 1]
|
||||
const key = currencyKey(currency)
|
||||
const isDefault = isDefaultToken(currency)
|
||||
const customAdded = isCustomAddedToken(allTokens, currency)
|
||||
const isDefault = isDefaultToken(defaultTokens, currency)
|
||||
const customAdded = Boolean(!isDefault && currency instanceof Token && allTokens[currency.address])
|
||||
const balance = currency === ETHER ? ETHBalance : allBalances[key]
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
@ -129,6 +131,7 @@ export default function CurrencyList({
|
||||
allTokens,
|
||||
chainId,
|
||||
currencies,
|
||||
defaultTokens,
|
||||
onCurrencySelect,
|
||||
otherCurrency,
|
||||
removeToken,
|
||||
|
@ -88,6 +88,9 @@ const MenuFlyout = styled.span`
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -5,6 +5,7 @@ import styled from 'styled-components'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useDefaultTokenList } from '../../state/lists/hooks'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
@ -67,8 +68,8 @@ interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'
|
||||
|
||||
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const isDefault = isDefaultToken(token)
|
||||
const defaultTokens = useDefaultTokenList()
|
||||
const isDefault = isDefaultToken(defaultTokens, token)
|
||||
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
|
@ -1,71 +0,0 @@
|
||||
import React, { useCallback, useContext, useState } from 'react'
|
||||
import { AlertCircle, CheckCircle } from 'react-feather'
|
||||
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
const Fader = styled.div<{ count: number }>`
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: ${({ count }) => `calc(100% - (100% / ${150 / count}))`};
|
||||
height: 2px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
transition: width 100ms linear;
|
||||
`
|
||||
|
||||
const delay = 100
|
||||
|
||||
export default function TxnPopup({
|
||||
hash,
|
||||
success,
|
||||
summary,
|
||||
popKey
|
||||
}: {
|
||||
hash: string
|
||||
success?: boolean
|
||||
summary?: string
|
||||
popKey?: string
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const [count, setCount] = useState(1)
|
||||
|
||||
const [isRunning, setIsRunning] = useState(true)
|
||||
const removePopup = useRemovePopup()
|
||||
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
count > 150 ? removeThisPopup() : setCount(count + 1)
|
||||
},
|
||||
isRunning ? delay : null
|
||||
)
|
||||
|
||||
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
|
||||
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<AutoRow onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<div style={{ paddingRight: 16 }}>
|
||||
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
|
||||
</div>
|
||||
<AutoColumn gap="8px">
|
||||
<TYPE.body fontWeight={500}>{summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
||||
</AutoColumn>
|
||||
<Fader count={count} />
|
||||
</AutoRow>
|
||||
)
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
|
||||
|
||||
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
|
||||
import { COMP, DAI, MKR, USDC, USDT } from './tokens/mainnet'
|
||||
|
||||
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
|
||||
|
||||
@ -10,6 +9,12 @@ type ChainTokenList = {
|
||||
readonly [chainId in ChainId]: Token[]
|
||||
}
|
||||
|
||||
export const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
|
||||
export const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||
export const USDT = new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD')
|
||||
export const COMP = new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound')
|
||||
export const MKR = new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker')
|
||||
|
||||
const WETH_ONLY: ChainTokenList = {
|
||||
[ChainId.MAINNET]: [WETH[ChainId.MAINNET]],
|
||||
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
|
||||
@ -142,3 +147,7 @@ export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(
|
||||
// used to ensure the user doesn't send so much ETH so they end up with <.01
|
||||
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
|
||||
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
|
||||
// the Uniswap Default token list lives here
|
||||
export const DEFAULT_TOKEN_LIST_URL =
|
||||
'https://unpkg.com/@uniswap/default-token-list@latest/uniswap-default.tokenlist.json'
|
||||
|
@ -1,36 +0,0 @@
|
||||
import { ChainId, Token, WETH } from '@uniswap/sdk'
|
||||
import KOVAN_TOKENS from './kovan'
|
||||
import MAINNET_TOKENS from './mainnet'
|
||||
import RINKEBY_TOKENS from './rinkeby'
|
||||
import ROPSTEN_TOKENS from './ropsten'
|
||||
|
||||
type AllTokens = Readonly<{ [chainId in ChainId]: Readonly<{ [tokenAddress: string]: Token }> }>
|
||||
export const ALL_TOKENS: AllTokens = [
|
||||
// WETH on all chains
|
||||
...Object.values(WETH),
|
||||
// chain-specific tokens
|
||||
...MAINNET_TOKENS,
|
||||
...RINKEBY_TOKENS,
|
||||
...KOVAN_TOKENS,
|
||||
...ROPSTEN_TOKENS
|
||||
]
|
||||
// put into an object
|
||||
.reduce<AllTokens>(
|
||||
(tokenMap, token) => {
|
||||
if (tokenMap[token.chainId][token.address] !== undefined) throw Error('Duplicate tokens.')
|
||||
return {
|
||||
...tokenMap,
|
||||
[token.chainId]: {
|
||||
...tokenMap[token.chainId],
|
||||
[token.address]: token
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
[ChainId.MAINNET]: {},
|
||||
[ChainId.RINKEBY]: {},
|
||||
[ChainId.GÖRLI]: {},
|
||||
[ChainId.ROPSTEN]: {},
|
||||
[ChainId.KOVAN]: {}
|
||||
}
|
||||
)
|
@ -1,6 +0,0 @@
|
||||
import { Token, ChainId } from '@uniswap/sdk'
|
||||
|
||||
export default [
|
||||
new Token(ChainId.KOVAN, '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa', 18, 'DAI', 'Dai Stablecoin'),
|
||||
new Token(ChainId.KOVAN, '0xAaF64BFCC32d0F15873a02163e7E500671a4ffcD', 18, 'MKR', 'Maker')
|
||||
]
|
@ -1,134 +0,0 @@
|
||||
import { Token, ChainId } from '@uniswap/sdk'
|
||||
|
||||
export const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
|
||||
export const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||
export const USDT = new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD')
|
||||
export const COMP = new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound')
|
||||
export const MKR = new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker')
|
||||
|
||||
export default [
|
||||
new Token(ChainId.MAINNET, '0xB6eD7644C69416d67B522e20bC294A9a9B405B31', 8, '0xBTC', '0xBitcoin Token'),
|
||||
new Token(ChainId.MAINNET, '0xfC1E690f61EFd961294b3e1Ce3313fBD8aa4f85d', 18, 'aDAI', 'Aave Interest bearing DAI'),
|
||||
new Token(ChainId.MAINNET, '0x737F98AC8cA59f2C68aD658E3C3d8C8963E40a4c', 18, 'AMN', 'Amon'),
|
||||
new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth'),
|
||||
new Token(ChainId.MAINNET, '0xcD62b1C403fa761BAadFC74C525ce2B51780b184', 18, 'ANJ', 'Aragon Network Juror'),
|
||||
new Token(ChainId.MAINNET, '0x960b236A07cf122663c4303350609A66A7B288C0', 18, 'ANT', 'Aragon Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x27054b13b1B798B345b591a4d22e6562d47eA75a', 4, 'AST', 'AirSwap Token'),
|
||||
new Token(ChainId.MAINNET, '0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55', 18, 'BAND', 'BandToken'),
|
||||
new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'),
|
||||
new Token(ChainId.MAINNET, '0xba100000625a3754423978a60c9317c58a424e3D', 18, 'BAL', 'Balancer'),
|
||||
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
|
||||
new Token(ChainId.MAINNET, '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 18, 'BNT', 'Bancor Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd', 18, 'BTC++', 'PieDAO BTC++'),
|
||||
new Token(ChainId.MAINNET, '0x56d811088235F11C8920698a204A5010a788f4b3', 18, 'BZRX', 'bZx Protocol Token'),
|
||||
new Token(ChainId.MAINNET, '0x4F9254C83EB525f9FCf346490bbb3ed28a81C667', 18, 'CELR', 'CelerToken'),
|
||||
new Token(ChainId.MAINNET, '0xF5DCe57282A584D2746FaF1593d3121Fcac444dC', 8, 'cSAI', 'Compound Dai'),
|
||||
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
|
||||
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
|
||||
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
|
||||
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
|
||||
COMP,
|
||||
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
|
||||
DAI,
|
||||
new Token(ChainId.MAINNET, '0x0Cf0Ee63788A0849fE5297F3407f701E122cC023', 18, 'DATA', 'Streamr DATAcoin'),
|
||||
new Token(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A', 9, 'DGD', 'DigixDAO'),
|
||||
new Token(ChainId.MAINNET, '0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF', 9, 'DGX', 'Digix Gold Token'),
|
||||
new Token(
|
||||
ChainId.MAINNET,
|
||||
'0xc719d010B63E5bbF2C0551872CD5316ED26AcD83',
|
||||
18,
|
||||
'DIP',
|
||||
'Decentralized Insurance Protocol'
|
||||
),
|
||||
new Token(ChainId.MAINNET, '0xC0F9bD5Fa5698B6505F643900FFA515Ea5dF54A9', 18, 'DONUT', 'Donut'),
|
||||
new Token(ChainId.MAINNET, '0x86FADb80d8D2cff3C3680819E4da99C10232Ba0F', 18, 'EBASE', 'EURBASE Stablecoin'),
|
||||
new Token(ChainId.MAINNET, '0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c', 18, 'ENJ', 'Enjin Coin'),
|
||||
new Token(ChainId.MAINNET, '0x06f65b8CfCb13a9FE37d836fE9708dA38Ecb29B2', 18, 'FAME', 'SAINT FAME: Genesis Shirt'),
|
||||
new Token(ChainId.MAINNET, '0x4946Fcea7C692606e8908002e55A582af44AC121', 18, 'FOAM', 'FOAM Token'),
|
||||
new Token(ChainId.MAINNET, '0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b', 8, 'FUN', 'FunFair'),
|
||||
new Token(ChainId.MAINNET, '0x4a57E687b9126435a9B19E4A802113e266AdeBde', 18, 'FXC', 'Flexacoin'),
|
||||
new Token(ChainId.MAINNET, '0x543Ff227F64Aa17eA132Bf9886cAb5DB55DCAddf', 18, 'GEN', 'DAOstack'),
|
||||
new Token(ChainId.MAINNET, '0x6810e776880C02933D47DB1b9fc05908e5386b96', 18, 'GNO', 'Gnosis Token'),
|
||||
new Token(ChainId.MAINNET, '0x12B19D3e2ccc14Da04FAe33e63652ce469b3F2FD', 12, 'GRID', 'GRID Token'),
|
||||
new Token(ChainId.MAINNET, '0x0000000000b3F879cb30FE243b4Dfee438691c04', 2, 'GST2', 'Gastoken.io'),
|
||||
new Token(ChainId.MAINNET, '0xF1290473E210b2108A85237fbCd7b6eb42Cc654F', 18, 'HEDG', 'HedgeTrade'),
|
||||
new Token(ChainId.MAINNET, '0x6c6EE5e31d828De241282B9606C8e98Ea48526E2', 18, 'HOT', 'HoloToken'),
|
||||
new Token(ChainId.MAINNET, '0x493C57C4763932315A328269E1ADaD09653B9081', 18, 'iDAI', 'Fulcrum DAI iToken'),
|
||||
new Token(ChainId.MAINNET, '0x14094949152EDDBFcd073717200DA82fEd8dC960', 18, 'iSAI', 'Fulcrum SAI iToken '),
|
||||
new Token(ChainId.MAINNET, '0x6fB3e0A217407EFFf7Ca062D46c26E5d60a14d69', 18, 'IOTX', 'IoTeX Network'),
|
||||
new Token(ChainId.MAINNET, '0x4Cd988AfBad37289BAAf53C13e98E2BD46aAEa8c', 18, 'KEY', 'KEY'),
|
||||
new Token(ChainId.MAINNET, '0xdd974D5C2e2928deA5F71b9825b8b646686BD200', 18, 'KNC', 'Kyber Network Crystal'),
|
||||
new Token(ChainId.MAINNET, '0x514910771AF9Ca656af840dff83E8264EcF986CA', 18, 'LINK', 'ChainLink Token'),
|
||||
new Token(ChainId.MAINNET, '0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD', 18, 'LRC', 'LoopringCoin V2'),
|
||||
new Token(ChainId.MAINNET, '0x80fB784B7eD66730e8b1DBd9820aFD29931aab03', 18, 'LEND', 'EthLend Token'),
|
||||
new Token(ChainId.MAINNET, '0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0', 18, 'LOOM', 'LoomToken'),
|
||||
new Token(ChainId.MAINNET, '0x58b6A8A3302369DAEc383334672404Ee733aB239', 18, 'LPT', 'Livepeer Token'),
|
||||
new Token(ChainId.MAINNET, '0xD29F0b5b3F50b07Fe9a9511F7d86F4f4bAc3f8c4', 18, 'LQD', 'Liquidity.Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x0F5D2fB29fb7d3CFeE444a200298f468908cC942', 18, 'MANA', 'Decentraland MANA'),
|
||||
new Token(ChainId.MAINNET, '0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0', 18, 'MATIC', 'Matic Token'),
|
||||
new Token(ChainId.MAINNET, '0x8888889213DD4dA823EbDD1e235b09590633C150', 18, 'MBC', 'Marblecoin'),
|
||||
new Token(ChainId.MAINNET, '0xd15eCDCF5Ea68e3995b2D0527A0aE0a3258302F8', 18, 'MCX', 'MachiX Token'),
|
||||
new Token(ChainId.MAINNET, '0xa3d58c4E56fedCae3a7c43A725aeE9A71F0ece4e', 18, 'MET', 'Metronome'),
|
||||
new Token(ChainId.MAINNET, '0x80f222a749a2e18Eb7f676D371F19ad7EFEEe3b7', 18, 'MGN', 'Magnolia Token'),
|
||||
MKR,
|
||||
new Token(ChainId.MAINNET, '0xec67005c4E498Ec7f55E092bd1d35cbC47C91892', 18, 'MLN', 'Melon Token'),
|
||||
new Token(ChainId.MAINNET, '0x957c30aB0426e0C93CD8241E2c60392d08c6aC8e', 0, 'MOD', 'Modum Token'),
|
||||
new Token(ChainId.MAINNET, '0xe2f2a5C287993345a840Db3B0845fbC70f5935a5', 18, 'mUSD', 'mStable USD'),
|
||||
new Token(ChainId.MAINNET, '0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206', 18, 'NEXO', 'Nexo'),
|
||||
new Token(ChainId.MAINNET, '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671', 18, 'NMR', 'Numeraire'),
|
||||
new Token(ChainId.MAINNET, '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41', 18, 'OCEAN', 'OceanToken'),
|
||||
new Token(ChainId.MAINNET, '0x4575f41308EC1483f3d399aa9a2826d74Da13Deb', 18, 'OXT', 'Orchid'),
|
||||
new Token(ChainId.MAINNET, '0xD56daC73A4d6766464b38ec6D91eB45Ce7457c44', 18, 'PAN', 'Panvala pan'),
|
||||
new Token(ChainId.MAINNET, '0x8E870D67F660D95d5be530380D0eC0bd388289E1', 18, 'PAX', 'PAX'),
|
||||
new Token(ChainId.MAINNET, '0x45804880De22913dAFE09f4980848ECE6EcbAf78', 18, 'PAXG', 'Paxos Gold'),
|
||||
new Token(ChainId.MAINNET, '0x93ED3FBe21207Ec2E8f2d3c3de6e058Cb73Bc04d', 18, 'PNK', 'Pinakion'),
|
||||
new Token(ChainId.MAINNET, '0x6758B7d441a9739b98552B373703d8d3d14f9e62', 18, 'POA20', 'POA ERC20 on Foundation'),
|
||||
new Token(ChainId.MAINNET, '0x687BfC3E73f6af55F0CccA8450114D107E781a0e', 18, 'QCH', 'QChi'),
|
||||
new Token(ChainId.MAINNET, '0x4a220E6096B25EADb88358cb44068A3248254675', 18, 'QNT', 'Quant'),
|
||||
new Token(ChainId.MAINNET, '0x99ea4dB9EE77ACD40B119BD1dC4E33e1C070b80d', 18, 'QSP', 'Quantstamp Token'),
|
||||
new Token(ChainId.MAINNET, '0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6', 18, 'RCN', 'Ripio Credit Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6', 18, 'RDN', 'Raiden Token'),
|
||||
new Token(ChainId.MAINNET, '0x408e41876cCCDC0F92210600ef50372656052a38', 18, 'REN', 'Republic Token'),
|
||||
new Token(ChainId.MAINNET, '0x459086F2376525BdCebA5bDDA135e4E9d3FeF5bf', 8, 'renBCH', 'renBCH'),
|
||||
new Token(ChainId.MAINNET, '0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D', 8, 'renBTC', 'renBTC'),
|
||||
new Token(ChainId.MAINNET, '0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2', 8, 'renZEC', 'renZEC'),
|
||||
new Token(ChainId.MAINNET, '0x1985365e9f78359a9B6AD760e32412f4a445E862', 18, 'REPv1', 'Augur v1 Reputation'),
|
||||
new Token(ChainId.MAINNET, '0x9469D013805bFfB7D3DEBe5E7839237e535ec483', 18, 'RING', 'Darwinia Network Native Token'),
|
||||
new Token(ChainId.MAINNET, '0x607F4C5BB672230e8672085532f7e901544a7375', 9, 'RLC', 'iEx.ec Network Token'),
|
||||
new Token(ChainId.MAINNET, '0xB4EFd85c19999D84251304bDA99E90B92300Bd93', 18, 'RPL', 'Rocket Pool'),
|
||||
new Token(ChainId.MAINNET, '0x4156D3342D5c385a87D264F90653733592000581', 8, 'SALT', 'Salt'),
|
||||
new Token(ChainId.MAINNET, '0x7C5A0CE9267ED19B22F8cae653F198e3E8daf098', 18, 'SAN', 'SANtiment network token'),
|
||||
new Token(ChainId.MAINNET, '0x5e74C9036fb86BD7eCdcb084a0673EFc32eA31cb', 18, 'sETH', 'Synth sETH'),
|
||||
new Token(ChainId.MAINNET, '0x3A9FfF453d50D4Ac52A6890647b823379ba36B9E', 18, 'SHUF', 'Shuffle.Monster V3'),
|
||||
new Token(ChainId.MAINNET, '0x744d70FDBE2Ba4CF95131626614a1763DF805B9E', 18, 'SNT', 'Status Network Token'),
|
||||
new Token(ChainId.MAINNET, '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 18, 'SNX', 'Synthetix Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x23B608675a2B2fB1890d3ABBd85c5775c51691d5', 18, 'SOCKS', 'Unisocks Edition 0'),
|
||||
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
|
||||
new Token(ChainId.MAINNET, '0x0Ae055097C6d159879521C384F1D2123D1f195e6', 18, 'STAKE', 'STAKE'),
|
||||
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
|
||||
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
|
||||
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
|
||||
new Token(ChainId.MAINNET, '0x8CE9137d39326AD0cD6491fb5CC0CbA0e089b6A9', 18, 'SXP', 'Swipe'),
|
||||
new Token(ChainId.MAINNET, '0x00006100F7090010005F1bd7aE6122c3C2CF0090', 18, 'TAUD', 'TrueAUD'),
|
||||
new Token(ChainId.MAINNET, '0x00000100F2A2bd000715001920eB70D229700085', 18, 'TCAD', 'TrueCAD'),
|
||||
new Token(ChainId.MAINNET, '0x00000000441378008EA67F4284A57932B1c000a5', 18, 'TGBP', 'TrueGBP'),
|
||||
new Token(ChainId.MAINNET, '0x0000852600CEB001E08e00bC008be620d60031F2', 18, 'THKD', 'TrueHKD'),
|
||||
new Token(ChainId.MAINNET, '0xaAAf91D9b90dF800Df4F55c205fd6989c977E73a', 8, 'TKN', 'Monolith TKN'),
|
||||
new Token(ChainId.MAINNET, '0x0Ba45A8b5d5575935B8158a88C631E9F9C95a2e5', 18, 'TRB', 'Tellor Tributes'),
|
||||
new Token(ChainId.MAINNET, '0xCb94be6f13A1182E4A4B6140cb7bf2025d28e41B', 6, 'TRST', 'Trustcoin'),
|
||||
new Token(ChainId.MAINNET, '0x2C537E5624e4af88A7ae4060C022609376C8D0EB', 6, 'TRYB', 'BiLira'),
|
||||
new Token(ChainId.MAINNET, '0x0000000000085d4780B73119b644AE5ecd22b376', 18, 'TUSD', 'TrueUSD'),
|
||||
new Token(ChainId.MAINNET, '0x8400D94A5cb0fa0D041a3788e395285d61c9ee5e', 8, 'UBT', 'UniBright'),
|
||||
new Token(ChainId.MAINNET, '0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828', 18, 'UMA', 'UMA Voting Token v1'),
|
||||
USDC,
|
||||
new Token(ChainId.MAINNET, '0xA4Bdb11dc0a2bEC88d24A3aa1E6Bb17201112eBe', 6, 'USDS', 'StableUSD'),
|
||||
USDT,
|
||||
new Token(ChainId.MAINNET, '0xeb269732ab75A6fD61Ea60b06fE994cD32a83549', 18, 'USDx', 'dForce'),
|
||||
new Token(ChainId.MAINNET, '0x9A48BD0EC040ea4f1D3147C025cd4076A2e71e3e', 18, 'USD++', 'PieDAO USD++'),
|
||||
new Token(ChainId.MAINNET, '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374', 18, 'VERI', 'Veritaseum'),
|
||||
new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC'),
|
||||
new Token(ChainId.MAINNET, '0x09fE5f0236F0Ea5D930197DCE254d77B04128075', 18, 'WCK', 'Wrapped CryptoKitties'),
|
||||
new Token(ChainId.MAINNET, '0xB4272071eCAdd69d933AdcD19cA99fe80664fc08', 18, 'XCHF', 'CryptoFranc'),
|
||||
new Token(ChainId.MAINNET, '0x0f7F961648aE6Db43C75663aC7E5414Eb79b5704', 18, 'XIO', 'XIO Network'),
|
||||
new Token(ChainId.MAINNET, '0xE41d2489571d322189246DaFA5ebDe1F4699F498', 18, 'ZRX', '0x Protocol Token')
|
||||
]
|
@ -1,6 +0,0 @@
|
||||
import { Token, ChainId } from '@uniswap/sdk'
|
||||
|
||||
export default [
|
||||
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin'),
|
||||
new Token(ChainId.RINKEBY, '0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85', 18, 'MKR', 'Maker')
|
||||
]
|
@ -1,3 +0,0 @@
|
||||
import { Token, ChainId } from '@uniswap/sdk'
|
||||
|
||||
export default [new Token(ChainId.ROPSTEN, '0xaD6D458402F60fD3Bd25163575031ACDce07538D', 18, 'DAI', 'Dai Stablecoin')]
|
@ -1,7 +1,7 @@
|
||||
import { parseBytes32String } from '@ethersproject/strings'
|
||||
import { ChainId, Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { ALL_TOKENS } from '../constants/tokens'
|
||||
import { useDefaultTokenList } from '../state/lists/hooks'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useUserAddedTokens } from '../state/user/hooks'
|
||||
import { isAddress } from '../utils'
|
||||
@ -12,6 +12,7 @@ import { useBytes32TokenContract, useTokenContract } from './useContract'
|
||||
export function useAllTokens(): { [address: string]: Token } {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
const allTokens = useDefaultTokenList()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!chainId) return {}
|
||||
@ -25,10 +26,10 @@ export function useAllTokens(): { [address: string]: Token } {
|
||||
},
|
||||
// must make a copy because reduce modifies the map, and we do not
|
||||
// want to make a copy in every iteration
|
||||
{ ...ALL_TOKENS[chainId as ChainId] }
|
||||
{ ...allTokens[chainId] }
|
||||
)
|
||||
)
|
||||
}, [userAddedTokens, chainId])
|
||||
}, [chainId, userAddedTokens, allTokens])
|
||||
}
|
||||
|
||||
// parse a name or symbol from a token response
|
||||
|
@ -72,21 +72,12 @@ export function useInactiveListener(suppress = false) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleNetworkChanged = () => {
|
||||
// eat errors
|
||||
activate(injected, undefined, true).catch(error => {
|
||||
console.error('Failed to activate after networks changed', error)
|
||||
})
|
||||
}
|
||||
|
||||
ethereum.on('chainChanged', handleChainChanged)
|
||||
ethereum.on('networkChanged', handleNetworkChanged)
|
||||
ethereum.on('accountsChanged', handleAccountsChanged)
|
||||
|
||||
return () => {
|
||||
if (ethereum.removeListener) {
|
||||
ethereum.removeListener('chainChanged', handleChainChanged)
|
||||
ethereum.removeListener('networkChanged', handleNetworkChanged)
|
||||
ethereum.removeListener('accountsChanged', handleAccountsChanged)
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,23 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
const VISIBILITY_STATE_SUPPORTED = 'visibilityState' in document
|
||||
|
||||
function isWindowVisible() {
|
||||
return !VISIBILITY_STATE_SUPPORTED || document.visibilityState !== 'hidden'
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the window is currently visible to the user.
|
||||
*/
|
||||
export default function useIsWindowVisible(): boolean {
|
||||
const [focused, setFocused] = useState<boolean>(true)
|
||||
const [focused, setFocused] = useState<boolean>(isWindowVisible())
|
||||
const listener = useCallback(() => {
|
||||
setFocused(document.visibilityState !== 'hidden')
|
||||
setFocused(isWindowVisible())
|
||||
}, [setFocused])
|
||||
|
||||
useEffect(() => {
|
||||
if (!VISIBILITY_STATE_SUPPORTED) return
|
||||
|
||||
document.addEventListener('visibilitychange', listener)
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', listener)
|
||||
|
@ -42,8 +42,12 @@ export default function useWrapCallback(
|
||||
execute:
|
||||
sufficientBalance && inputAmount
|
||||
? async () => {
|
||||
const txReceipt = await wethContract.deposit({ value: `0x${inputAmount.raw.toString(16)}` })
|
||||
addTransaction(txReceipt, { summary: `Wrap ${inputAmount.toSignificant(6)} ETH to WETH` })
|
||||
try {
|
||||
const txReceipt = await wethContract.deposit({ value: `0x${inputAmount.raw.toString(16)}` })
|
||||
addTransaction(txReceipt, { summary: `Wrap ${inputAmount.toSignificant(6)} ETH to WETH` })
|
||||
} catch (error) {
|
||||
console.error('Could not deposit', error)
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
error: sufficientBalance ? undefined : 'Insufficient ETH balance'
|
||||
@ -54,8 +58,12 @@ export default function useWrapCallback(
|
||||
execute:
|
||||
sufficientBalance && inputAmount
|
||||
? async () => {
|
||||
const txReceipt = await wethContract.withdraw(`0x${inputAmount.raw.toString(16)}`)
|
||||
addTransaction(txReceipt, { summary: `Unwrap ${inputAmount.toSignificant(6)} WETH to ETH` })
|
||||
try {
|
||||
const txReceipt = await wethContract.withdraw(`0x${inputAmount.raw.toString(16)}`)
|
||||
addTransaction(txReceipt, { summary: `Unwrap ${inputAmount.toSignificant(6)} WETH to ETH` })
|
||||
} catch (error) {
|
||||
console.error('Could not withdraw', error)
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
error: sufficientBalance ? undefined : 'Insufficient WETH balance'
|
||||
|
@ -12,12 +12,17 @@ import App from './pages/App'
|
||||
import store from './state'
|
||||
import ApplicationUpdater from './state/application/updater'
|
||||
import TransactionUpdater from './state/transactions/updater'
|
||||
import ListsUpdater from './state/lists/updater'
|
||||
import UserUpdater from './state/user/updater'
|
||||
import MulticallUpdater from './state/multicall/updater'
|
||||
import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
|
||||
|
||||
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
|
||||
|
||||
if ('ethereum' in window) {
|
||||
;(window.ethereum as any).autoRefreshOnNetworkChange = false
|
||||
}
|
||||
|
||||
function getLibrary(provider: any): Web3Provider {
|
||||
const library = new Web3Provider(provider)
|
||||
library.pollingInterval = 15000
|
||||
@ -44,6 +49,7 @@ window.addEventListener('error', error => {
|
||||
function Updaters() {
|
||||
return (
|
||||
<>
|
||||
<ListsUpdater />
|
||||
<UserUpdater />
|
||||
<ApplicationUpdater />
|
||||
<TransactionUpdater />
|
||||
|
@ -6,7 +6,8 @@ import { AutoRow } from '../../components/Row'
|
||||
import { SearchInput } from '../../components/SearchModal/styleds'
|
||||
import { useAllTokenV1Exchanges } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useToken, useAllTokens } from '../../hooks/Tokens'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import { useDefaultTokenList } from '../../state/lists/hooks'
|
||||
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
|
||||
import { BackArrow, TYPE } from '../../theme'
|
||||
import { LightCard } from '../../components/Card'
|
||||
@ -16,7 +17,7 @@ import V1PositionCard from '../../components/PositionCard/V1'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { useAddUserToken } from '../../state/user/hooks'
|
||||
import { isDefaultToken, isCustomAddedToken } from '../../utils'
|
||||
import { isDefaultToken } from '../../utils'
|
||||
|
||||
export default function MigrateV1() {
|
||||
const theme = useContext(ThemeContext)
|
||||
@ -27,15 +28,15 @@ export default function MigrateV1() {
|
||||
|
||||
// automatically add the search token
|
||||
const token = useToken(tokenSearch)
|
||||
const isDefault = isDefaultToken(token)
|
||||
const defaultTokens = useDefaultTokenList()
|
||||
const isDefault = isDefaultToken(defaultTokens, token)
|
||||
const allTokens = useAllTokens()
|
||||
const isCustomAdded = isCustomAddedToken(allTokens, token)
|
||||
const addToken = useAddUserToken()
|
||||
useEffect(() => {
|
||||
if (token && !isDefault && !isCustomAdded) {
|
||||
if (token && !isDefault && !allTokens[token.address]) {
|
||||
addToken(token)
|
||||
}
|
||||
}, [token, isDefault, isCustomAdded, addToken])
|
||||
}, [token, isDefault, addToken, allTokens])
|
||||
|
||||
// get V1 LP balances
|
||||
const V1Exchanges = useAllTokenV1Exchanges()
|
||||
|
@ -1,12 +1,22 @@
|
||||
import { createAction } from '@reduxjs/toolkit'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
|
||||
export type PopupContent = {
|
||||
txn: {
|
||||
hash: string
|
||||
success: boolean
|
||||
summary?: string
|
||||
}
|
||||
}
|
||||
export type PopupContent =
|
||||
| {
|
||||
txn: {
|
||||
hash: string
|
||||
success: boolean
|
||||
summary?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
listUpdate: {
|
||||
listUrl: string
|
||||
oldList: TokenList
|
||||
newList: TokenList
|
||||
auto: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('updateBlockNumber')
|
||||
export const toggleWalletModal = createAction<void>('toggleWalletModal')
|
||||
|
92
src/state/application/reducer.test.ts
Normal file
92
src/state/application/reducer.test.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import { createStore, Store } from 'redux'
|
||||
import { addPopup, removePopup, toggleSettingsMenu, toggleWalletModal, updateBlockNumber } from './actions'
|
||||
import reducer, { ApplicationState } from './reducer'
|
||||
|
||||
describe('application reducer', () => {
|
||||
let store: Store<ApplicationState>
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer, {
|
||||
popupList: [],
|
||||
walletModalOpen: false,
|
||||
settingsMenuOpen: false,
|
||||
blockNumber: {
|
||||
[ChainId.MAINNET]: 3
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('addPopup', () => {
|
||||
it('adds the popup to list with a generated id', () => {
|
||||
store.dispatch(addPopup({ content: { txn: { hash: 'abc', summary: 'test', success: true } } }))
|
||||
const list = store.getState().popupList
|
||||
expect(list).toHaveLength(1)
|
||||
expect(typeof list[0].key).toEqual('string')
|
||||
expect(list[0].show).toEqual(true)
|
||||
expect(list[0].content).toEqual({ txn: { hash: 'abc', summary: 'test', success: true } })
|
||||
})
|
||||
|
||||
it('replaces any existing popups with the same key', () => {
|
||||
store.dispatch(addPopup({ key: 'abc', content: { txn: { hash: 'abc', summary: 'test', success: true } } }))
|
||||
store.dispatch(addPopup({ key: 'abc', content: { txn: { hash: 'def', summary: 'test2', success: false } } }))
|
||||
const list = store.getState().popupList
|
||||
expect(list).toHaveLength(1)
|
||||
expect(list[0].key).toEqual('abc')
|
||||
expect(list[0].show).toEqual(true)
|
||||
expect(list[0].content).toEqual({ txn: { hash: 'def', summary: 'test2', success: false } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('toggleWalletModal', () => {
|
||||
it('toggles wallet modal', () => {
|
||||
store.dispatch(toggleWalletModal())
|
||||
expect(store.getState().walletModalOpen).toEqual(true)
|
||||
store.dispatch(toggleWalletModal())
|
||||
expect(store.getState().walletModalOpen).toEqual(false)
|
||||
store.dispatch(toggleWalletModal())
|
||||
expect(store.getState().walletModalOpen).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('settingsMenuOpen', () => {
|
||||
it('toggles settings menu', () => {
|
||||
store.dispatch(toggleSettingsMenu())
|
||||
expect(store.getState().settingsMenuOpen).toEqual(true)
|
||||
store.dispatch(toggleSettingsMenu())
|
||||
expect(store.getState().settingsMenuOpen).toEqual(false)
|
||||
store.dispatch(toggleSettingsMenu())
|
||||
expect(store.getState().settingsMenuOpen).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateBlockNumber', () => {
|
||||
it('updates block number', () => {
|
||||
store.dispatch(updateBlockNumber({ chainId: ChainId.MAINNET, blockNumber: 4 }))
|
||||
expect(store.getState().blockNumber[ChainId.MAINNET]).toEqual(4)
|
||||
})
|
||||
it('no op if late', () => {
|
||||
store.dispatch(updateBlockNumber({ chainId: ChainId.MAINNET, blockNumber: 2 }))
|
||||
expect(store.getState().blockNumber[ChainId.MAINNET]).toEqual(3)
|
||||
})
|
||||
it('works with non-set chains', () => {
|
||||
store.dispatch(updateBlockNumber({ chainId: ChainId.ROPSTEN, blockNumber: 2 }))
|
||||
expect(store.getState().blockNumber).toEqual({
|
||||
[ChainId.MAINNET]: 3,
|
||||
[ChainId.ROPSTEN]: 2
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removePopup', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch(addPopup({ key: 'abc', content: { txn: { hash: 'abc', summary: 'test', success: true } } }))
|
||||
})
|
||||
it('hides the popup', () => {
|
||||
expect(store.getState().popupList[0].show).toBe(true)
|
||||
store.dispatch(removePopup({ key: 'abc' }))
|
||||
expect(store.getState().popupList).toHaveLength(1)
|
||||
expect(store.getState().popupList[0].show).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
@ -10,7 +10,7 @@ import {
|
||||
|
||||
type PopupList = Array<{ key: string; show: boolean; content: PopupContent }>
|
||||
|
||||
interface ApplicationState {
|
||||
export interface ApplicationState {
|
||||
blockNumber: { [chainId: number]: number }
|
||||
popupList: PopupList
|
||||
walletModalOpen: boolean
|
||||
@ -41,12 +41,13 @@ export default createReducer(initialState, builder =>
|
||||
state.settingsMenuOpen = !state.settingsMenuOpen
|
||||
})
|
||||
.addCase(addPopup, (state, { payload: { content, key } }) => {
|
||||
if (key && state.popupList.some(popup => popup.key === key)) return
|
||||
state.popupList.push({
|
||||
key: key || nanoid(),
|
||||
show: true,
|
||||
content
|
||||
})
|
||||
state.popupList = (key ? state.popupList.filter(popup => popup.key !== key) : state.popupList).concat([
|
||||
{
|
||||
key: key || nanoid(),
|
||||
show: true,
|
||||
content
|
||||
}
|
||||
])
|
||||
})
|
||||
.addCase(removePopup, (state, { payload: { key } }) => {
|
||||
state.popupList.forEach(p => {
|
||||
|
@ -6,12 +6,13 @@ import user from './user/reducer'
|
||||
import transactions from './transactions/reducer'
|
||||
import swap from './swap/reducer'
|
||||
import mint from './mint/reducer'
|
||||
import lists from './lists/reducer'
|
||||
import burn from './burn/reducer'
|
||||
import multicall from './multicall/reducer'
|
||||
|
||||
import { updateVersion } from './user/actions'
|
||||
|
||||
const PERSISTED_KEYS: string[] = ['user', 'transactions']
|
||||
const PERSISTED_KEYS: string[] = ['user', 'transactions', 'lists']
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
@ -21,7 +22,8 @@ const store = configureStore({
|
||||
swap,
|
||||
mint,
|
||||
burn,
|
||||
multicall
|
||||
multicall,
|
||||
lists
|
||||
},
|
||||
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
|
||||
preloadedState: load({ states: PERSISTED_KEYS })
|
||||
|
54
src/state/lists/actions.ts
Normal file
54
src/state/lists/actions.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { createAction, createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import { TokenList, Version } from '@uniswap/token-lists'
|
||||
import schema from '@uniswap/token-lists/src/tokenlist.schema.json'
|
||||
import Ajv from 'ajv'
|
||||
import uriToHttp from '../../utils/uriToHttp'
|
||||
|
||||
const tokenListValidator = new Ajv({ allErrors: true }).compile(schema)
|
||||
|
||||
/**
|
||||
* Contains the logic for resolving a URL to a valid token list
|
||||
* @param listUrl list url
|
||||
*/
|
||||
async function getTokenList(listUrl: string): Promise<TokenList> {
|
||||
const urls = uriToHttp(listUrl)
|
||||
for (const url of urls) {
|
||||
let response
|
||||
try {
|
||||
response = await fetch(url)
|
||||
if (!response.ok) continue
|
||||
} catch (error) {
|
||||
console.error(`failed to fetch list ${listUrl} at uri ${url}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
if (!tokenListValidator(json)) {
|
||||
throw new Error(
|
||||
tokenListValidator.errors?.reduce<string>((memo, error) => {
|
||||
const add = `${error.dataPath} ${error.message ?? ''}`
|
||||
return memo.length > 0 ? `${memo}; ${add}` : `${add}`
|
||||
}, '') ?? 'Token list failed validation'
|
||||
)
|
||||
}
|
||||
return json
|
||||
}
|
||||
throw new Error('Unrecognized list URL protocol.')
|
||||
}
|
||||
|
||||
const fetchCache: { [url: string]: Promise<TokenList> } = {}
|
||||
export const fetchTokenList = createAsyncThunk<TokenList, string>(
|
||||
'lists/fetchTokenList',
|
||||
(url: string) =>
|
||||
// this makes it so we only ever fetch a list a single time concurrently
|
||||
(fetchCache[url] =
|
||||
fetchCache[url] ??
|
||||
getTokenList(url).catch(error => {
|
||||
delete fetchCache[url]
|
||||
throw error
|
||||
}))
|
||||
)
|
||||
|
||||
export const acceptListUpdate = createAction<string>('lists/acceptListUpdate')
|
||||
export const addList = createAction<string>('lists/addList')
|
||||
export const rejectVersionUpdate = createAction<Version>('lists/rejectVersionUpdate')
|
84
src/state/lists/hooks.ts
Normal file
84
src/state/lists/hooks.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { ChainId, Token } from '@uniswap/sdk'
|
||||
import { TokenInfo, TokenList } from '@uniswap/token-lists'
|
||||
import { useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { DEFAULT_TOKEN_LIST_URL } from '../../constants'
|
||||
import { AppState } from '../index'
|
||||
|
||||
/**
|
||||
* Token instances created from token info.
|
||||
*/
|
||||
export class WrappedTokenInfo extends Token {
|
||||
public readonly tokenInfo: TokenInfo
|
||||
constructor(tokenInfo: TokenInfo) {
|
||||
super(tokenInfo.chainId, tokenInfo.address, tokenInfo.decimals, tokenInfo.symbol, tokenInfo.name)
|
||||
this.tokenInfo = tokenInfo
|
||||
}
|
||||
public get logoURI(): string | undefined {
|
||||
return this.tokenInfo.logoURI
|
||||
}
|
||||
}
|
||||
|
||||
export type TokenAddressMap = Readonly<{ [chainId in ChainId]: Readonly<{ [tokenAddress: string]: WrappedTokenInfo }> }>
|
||||
|
||||
/**
|
||||
* An empty result, useful as a default.
|
||||
*/
|
||||
const EMPTY_LIST: TokenAddressMap = {
|
||||
[ChainId.KOVAN]: {},
|
||||
[ChainId.RINKEBY]: {},
|
||||
[ChainId.ROPSTEN]: {},
|
||||
[ChainId.GÖRLI]: {},
|
||||
[ChainId.MAINNET]: {}
|
||||
}
|
||||
|
||||
const listCache: WeakMap<TokenList, TokenAddressMap> | null =
|
||||
'WeakMap' in window ? new WeakMap<TokenList, TokenAddressMap>() : null
|
||||
|
||||
export function listToTokenMap(list: TokenList): TokenAddressMap {
|
||||
const result = listCache?.get(list)
|
||||
if (result) return result
|
||||
|
||||
const map = list.tokens.reduce<TokenAddressMap>(
|
||||
(tokenMap, tokenInfo) => {
|
||||
const token = new WrappedTokenInfo(tokenInfo)
|
||||
if (tokenMap[token.chainId][token.address] !== undefined) throw Error('Duplicate tokens.')
|
||||
return {
|
||||
...tokenMap,
|
||||
[token.chainId]: {
|
||||
...tokenMap[token.chainId],
|
||||
[token.address]: token
|
||||
}
|
||||
}
|
||||
},
|
||||
{ ...EMPTY_LIST }
|
||||
)
|
||||
listCache?.set(list, map)
|
||||
return map
|
||||
}
|
||||
|
||||
export function useTokenList(url: string): TokenAddressMap {
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
return useMemo(() => {
|
||||
const current = lists[url]?.current
|
||||
if (!current) return EMPTY_LIST
|
||||
return listToTokenMap(current)
|
||||
}, [lists, url])
|
||||
}
|
||||
|
||||
export function useDefaultTokenList(): TokenAddressMap {
|
||||
return useTokenList(DEFAULT_TOKEN_LIST_URL)
|
||||
}
|
||||
|
||||
// returns all downloaded current lists
|
||||
export function useAllLists(): TokenList[] {
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.keys(lists)
|
||||
.map(url => lists[url].current)
|
||||
.filter((l): l is TokenList => Boolean(l)),
|
||||
[lists]
|
||||
)
|
||||
}
|
251
src/state/lists/reducer.test.ts
Normal file
251
src/state/lists/reducer.test.ts
Normal file
@ -0,0 +1,251 @@
|
||||
import { createStore, Store } from 'redux'
|
||||
import { fetchTokenList, acceptListUpdate, addList } from './actions'
|
||||
import reducer, { ListsState } from './reducer'
|
||||
|
||||
const STUB_TOKEN_LIST = {
|
||||
name: '',
|
||||
timestamp: '',
|
||||
version: { major: 1, minor: 1, patch: 1 },
|
||||
tokens: []
|
||||
}
|
||||
|
||||
const PATCHED_STUB_LIST = {
|
||||
...STUB_TOKEN_LIST,
|
||||
version: { ...STUB_TOKEN_LIST.version, patch: STUB_TOKEN_LIST.version.patch + 1 }
|
||||
}
|
||||
const MINOR_UPDATED_STUB_LIST = {
|
||||
...STUB_TOKEN_LIST,
|
||||
version: { ...STUB_TOKEN_LIST.version, minor: STUB_TOKEN_LIST.version.minor + 1 }
|
||||
}
|
||||
const MAJOR_UPDATED_STUB_LIST = {
|
||||
...STUB_TOKEN_LIST,
|
||||
version: { ...STUB_TOKEN_LIST.version, major: STUB_TOKEN_LIST.version.major + 1 }
|
||||
}
|
||||
|
||||
describe('list reducer', () => {
|
||||
let store: Store<ListsState>
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {}
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchTokenList', () => {
|
||||
describe('pending', () => {
|
||||
it('sets pending', () => {
|
||||
store.dispatch(fetchTokenList.pending('request-id', 'fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
loadingRequestId: 'request-id',
|
||||
current: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('does not clear current list', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
pendingUpdate: null,
|
||||
loadingRequestId: null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
store.dispatch(fetchTokenList.pending('request-id', 'fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: 'request-id',
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('fulfilled', () => {
|
||||
it('saves the list', () => {
|
||||
store.dispatch(fetchTokenList.fulfilled(STUB_TOKEN_LIST, 'request-id', 'fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('does not save the list in pending if current is same', () => {
|
||||
store.dispatch(fetchTokenList.fulfilled(STUB_TOKEN_LIST, 'request-id', 'fake-url'))
|
||||
store.dispatch(fetchTokenList.fulfilled(STUB_TOKEN_LIST, 'request-id', 'fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('does not save to current if list is newer patch version', () => {
|
||||
store.dispatch(fetchTokenList.fulfilled(STUB_TOKEN_LIST, 'request-id', 'fake-url'))
|
||||
|
||||
store.dispatch(fetchTokenList.fulfilled(PATCHED_STUB_LIST, 'request-id', 'fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
it('does not save to current if list is newer minor version', () => {
|
||||
store.dispatch(fetchTokenList.fulfilled(STUB_TOKEN_LIST, 'request-id', 'fake-url'))
|
||||
|
||||
store.dispatch(fetchTokenList.fulfilled(MINOR_UPDATED_STUB_LIST, 'request-id', 'fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: MINOR_UPDATED_STUB_LIST
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
it('does not save to pending if list is newer major version', () => {
|
||||
store.dispatch(fetchTokenList.fulfilled(STUB_TOKEN_LIST, 'request-id', 'fake-url'))
|
||||
|
||||
store.dispatch(fetchTokenList.fulfilled(MAJOR_UPDATED_STUB_LIST, 'request-id', 'fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: MAJOR_UPDATED_STUB_LIST
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('rejected', () => {
|
||||
it('no-op if not loading', () => {
|
||||
store.dispatch(fetchTokenList.rejected(new Error('abcd'), 'request-id', 'fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {}
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the error if loading', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: 'request-id',
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
})
|
||||
store.dispatch(fetchTokenList.rejected(new Error('abcd'), 'request-id', 'fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: 'abcd',
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addList', () => {
|
||||
it('adds the list key to byUrl', () => {
|
||||
store.dispatch(addList('list-id'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'list-id': {
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
it('no op for existing list', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
})
|
||||
store.dispatch(addList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('acceptListUpdate', () => {
|
||||
it('swaps pending update into current', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
}
|
||||
})
|
||||
store.dispatch(acceptListUpdate('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: PATCHED_STUB_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
90
src/state/lists/reducer.ts
Normal file
90
src/state/lists/reducer.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { getVersionUpgrade, VersionUpgrade } from '@uniswap/token-lists'
|
||||
import { TokenList } from '@uniswap/token-lists/dist/types'
|
||||
import { acceptListUpdate, addList, fetchTokenList } from './actions'
|
||||
|
||||
export interface ListsState {
|
||||
readonly byUrl: {
|
||||
readonly [url: string]: {
|
||||
readonly current: TokenList | null
|
||||
readonly pendingUpdate: TokenList | null
|
||||
readonly loadingRequestId: string | null
|
||||
readonly error: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: ListsState = {
|
||||
byUrl: {}
|
||||
}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
builder
|
||||
.addCase(fetchTokenList.pending, (state, { meta: { arg: url, requestId } }) => {
|
||||
state.byUrl[url] = {
|
||||
current: null,
|
||||
pendingUpdate: null,
|
||||
...state.byUrl[url],
|
||||
loadingRequestId: requestId,
|
||||
error: null
|
||||
}
|
||||
})
|
||||
.addCase(fetchTokenList.fulfilled, (state, { payload: tokenList, meta: { arg: url } }) => {
|
||||
const current = state.byUrl[url]?.current
|
||||
|
||||
// no-op if update does nothing
|
||||
if (current) {
|
||||
const type = getVersionUpgrade(current.version, tokenList.version)
|
||||
if (type === VersionUpgrade.NONE) return
|
||||
state.byUrl[url] = {
|
||||
...state.byUrl[url],
|
||||
loadingRequestId: null,
|
||||
error: null,
|
||||
current: current,
|
||||
pendingUpdate: tokenList
|
||||
}
|
||||
} else {
|
||||
state.byUrl[url] = {
|
||||
...state.byUrl[url],
|
||||
loadingRequestId: null,
|
||||
error: null,
|
||||
current: tokenList,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
})
|
||||
.addCase(fetchTokenList.rejected, (state, { error, meta: { requestId, arg: url } }) => {
|
||||
if (state.byUrl[url]?.loadingRequestId !== requestId) {
|
||||
// no-op since it's not the latest request
|
||||
return
|
||||
}
|
||||
|
||||
state.byUrl[url] = {
|
||||
...state.byUrl[url],
|
||||
loadingRequestId: null,
|
||||
error: error.message ?? 'Unknown error',
|
||||
current: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
})
|
||||
.addCase(addList, (state, { payload: url }) => {
|
||||
if (!state.byUrl[url]) {
|
||||
state.byUrl[url] = {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null,
|
||||
current: null,
|
||||
error: null
|
||||
}
|
||||
}
|
||||
})
|
||||
.addCase(acceptListUpdate, (state, { payload: url }) => {
|
||||
if (!state.byUrl[url]?.pendingUpdate) {
|
||||
throw new Error('accept list update called without pending update')
|
||||
}
|
||||
state.byUrl[url] = {
|
||||
...state.byUrl[url],
|
||||
pendingUpdate: null,
|
||||
current: state.byUrl[url].pendingUpdate
|
||||
}
|
||||
})
|
||||
)
|
92
src/state/lists/updater.ts
Normal file
92
src/state/lists/updater.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists'
|
||||
import { useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { DEFAULT_TOKEN_LIST_URL } from '../../constants'
|
||||
import { addPopup } from '../application/actions'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { acceptListUpdate, addList, fetchTokenList } from './actions'
|
||||
|
||||
export default function Updater(): null {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
|
||||
// we should always fetch the default token list, so add it
|
||||
useEffect(() => {
|
||||
if (!lists[DEFAULT_TOKEN_LIST_URL]) dispatch(addList(DEFAULT_TOKEN_LIST_URL))
|
||||
}, [dispatch, lists])
|
||||
|
||||
// on initial mount, refetch all the lists in storage
|
||||
useEffect(() => {
|
||||
Object.keys(lists).forEach(listUrl => dispatch(fetchTokenList(listUrl) as any))
|
||||
// we only do this once
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch])
|
||||
|
||||
// whenever a list is not loaded and not loading, try again to load it
|
||||
useEffect(() => {
|
||||
Object.keys(lists).forEach(listUrl => {
|
||||
const list = lists[listUrl]
|
||||
if (!list.current && !list.loadingRequestId && !list.error) {
|
||||
dispatch(fetchTokenList(listUrl) as any)
|
||||
}
|
||||
})
|
||||
}, [dispatch, lists])
|
||||
|
||||
// automatically update lists if versions are minor/patch
|
||||
useEffect(() => {
|
||||
Object.keys(lists).forEach(listUrl => {
|
||||
const list = lists[listUrl]
|
||||
if (list.current && list.pendingUpdate) {
|
||||
const bump = getVersionUpgrade(list.current.version, list.pendingUpdate.version)
|
||||
switch (bump) {
|
||||
case VersionUpgrade.NONE:
|
||||
throw new Error('unexpected no version bump')
|
||||
case VersionUpgrade.PATCH:
|
||||
case VersionUpgrade.MINOR:
|
||||
case VersionUpgrade.MAJOR:
|
||||
const min = minVersionBump(list.current.tokens, list.pendingUpdate.tokens)
|
||||
// automatically update minor/patch as long as bump matches the min update
|
||||
if (bump >= min) {
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
dispatch(
|
||||
addPopup({
|
||||
key: listUrl,
|
||||
content: {
|
||||
listUpdate: {
|
||||
listUrl,
|
||||
oldList: list.current,
|
||||
newList: list.pendingUpdate,
|
||||
auto: true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
console.error(
|
||||
`List at url ${listUrl} could not automatically update because the version bump was only PATCH/MINOR while the update had breaking changes and should have been MAJOR`
|
||||
)
|
||||
}
|
||||
break
|
||||
|
||||
// this will be turned on later
|
||||
// case VersionUpgrade.MAJOR:
|
||||
// dispatch(
|
||||
// addPopup({
|
||||
// key: listUrl,
|
||||
// content: {
|
||||
// listUpdate: {
|
||||
// listUrl,
|
||||
// auto: false,
|
||||
// oldList: list.current,
|
||||
// newList: list.pendingUpdate
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// )
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [dispatch, lists])
|
||||
|
||||
return null
|
||||
}
|
@ -13,7 +13,7 @@ export default function Updater() {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const transactions = useSelector<AppState, AppState['transactions']>(state => state.transactions)
|
||||
|
||||
const allTransactions = transactions[chainId ?? -1] ?? {}
|
||||
const allTransactions = chainId ? transactions[chainId] ?? {} : {}
|
||||
|
||||
// show popup on confirm
|
||||
const addPopup = useAddPopup()
|
||||
|
@ -214,7 +214,7 @@ export function useTrackedTokenPairs(): [Token, Token][] {
|
||||
(BASES_TO_TRACK_LIQUIDITY_FOR[chainId] ?? [])
|
||||
// to construct pairs of the given token with each base
|
||||
.map(base => {
|
||||
if (base.equals(token)) {
|
||||
if (base.address === token.address) {
|
||||
return null
|
||||
} else {
|
||||
return [base, token]
|
||||
|
@ -20,7 +20,11 @@ describe('swap reducer', () => {
|
||||
expect(store.getState().lastUpdateVersionTimestamp).toBeGreaterThanOrEqual(time)
|
||||
})
|
||||
it('sets allowed slippage and deadline', () => {
|
||||
store = createStore(reducer, { ...initialState, userDeadline: undefined, userSlippageTolerance: undefined })
|
||||
store = createStore(reducer, {
|
||||
...initialState,
|
||||
userDeadline: undefined,
|
||||
userSlippageTolerance: undefined
|
||||
} as any)
|
||||
store.dispatch(updateVersion())
|
||||
expect(store.getState().userDeadline).toEqual(DEFAULT_DEADLINE_FROM_NOW)
|
||||
expect(store.getState().userSlippageTolerance).toEqual(INITIAL_ALLOWED_SLIPPAGE)
|
||||
|
@ -5,8 +5,8 @@ import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
|
||||
import { ROUTER_ADDRESS } from '../constants'
|
||||
import { ALL_TOKENS } from '../constants/tokens'
|
||||
import { ChainId, JSBI, Percent, Token, CurrencyAmount, Currency, ETHER } from '@uniswap/sdk'
|
||||
import { TokenAddressMap } from '../state/lists/hooks'
|
||||
|
||||
// returns the checksummed address if the address is valid, otherwise returns false
|
||||
export function isAddress(value: any): string | false {
|
||||
@ -87,7 +87,7 @@ export function getContract(address: string, ABI: any, library: Web3Provider, ac
|
||||
throw Error(`Invalid 'address' parameter '${address}'.`)
|
||||
}
|
||||
|
||||
return new Contract(address, ABI, getProviderOrSigner(library, account))
|
||||
return new Contract(address, ABI, getProviderOrSigner(library, account) as any)
|
||||
}
|
||||
|
||||
// account is optional
|
||||
@ -99,12 +99,7 @@ export function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||
}
|
||||
|
||||
export function isDefaultToken(currency?: Currency): boolean {
|
||||
export function isDefaultToken(defaultTokens: TokenAddressMap, currency?: Currency): boolean {
|
||||
if (currency === ETHER) return true
|
||||
return Boolean(currency instanceof Token && ALL_TOKENS[currency.chainId]?.[currency.address])
|
||||
}
|
||||
|
||||
export function isCustomAddedToken(allTokens: { [address: string]: Token }, currency?: Currency): boolean {
|
||||
const isDefault = isDefaultToken(currency)
|
||||
return Boolean(!isDefault && currency instanceof Token && allTokens[currency.address])
|
||||
return Boolean(currency instanceof Token && defaultTokens[currency.chainId]?.[currency.address])
|
||||
}
|
||||
|
29
src/utils/uriToHttp.ts
Normal file
29
src/utils/uriToHttp.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Given a URI that may be ipfs, or http, or an ENS name, return the fetchable http(s) URLs for the same content
|
||||
* @param uri to convert to http url
|
||||
*/
|
||||
export default function uriToHttp(uri: string): string[] {
|
||||
try {
|
||||
const parsed = new URL(uri)
|
||||
if (parsed.protocol === 'http:') {
|
||||
return ['https' + uri.substr(4), uri]
|
||||
} else if (parsed.protocol === 'https:') {
|
||||
return [uri]
|
||||
} else if (parsed.protocol === 'ipfs:') {
|
||||
const hash = parsed.pathname.substring(2)
|
||||
return [`https://cloudflare-ipfs.com/ipfs/${hash}/`, `https://ipfs.infura.io/ipfs/${hash}/`]
|
||||
} else if (parsed.protocol === 'ipns:') {
|
||||
const name = parsed.pathname.substring(2)
|
||||
return [`https://cloudflare-ipfs.com/ipns/${name}/`, `https://ipfs.infura.io/ipns/${name}/`]
|
||||
} else {
|
||||
console.error('Unrecognized protocol', parsed)
|
||||
return []
|
||||
}
|
||||
} catch (error) {
|
||||
if (uri.toLowerCase().endsWith('.eth')) {
|
||||
return [`https://${uri.toLowerCase()}.link`]
|
||||
}
|
||||
console.error('Failed to parse URI', error)
|
||||
return []
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { CurrencyAmount, ETHER, Percent, Route, TokenAmount, Trade } from '@uniswap/sdk'
|
||||
import { DAI, USDC } from '../constants/tokens/mainnet'
|
||||
import { DAI, USDC } from '../constants'
|
||||
import { MockV1Pair } from '../data/V1'
|
||||
import v1SwapArguments from './v1SwapArguments'
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user