Compare commits

...

9 Commits

Author SHA1 Message Date
Moody Salem
3a36ac5538 chore(release): update dns again 2020-07-27 12:33:31 -05:00
Moody Salem
2962cd0e14 fix(migrate v1): migrate v1 pages and formatting 2020-07-27 12:33:02 -05:00
Moody Salem
6a311aa6d7 fix(v1 swap): exact out swaps not working 2020-07-27 08:45:48 -05:00
Moody Salem
e78b6d61f2 improvement(transactions): some clean up and unit tests
- fetch transaction state less often for old transactions
- fix a bug calling non payable methods with value 0
2020-07-27 08:45:48 -05:00
Moody Salem
365b429c0b 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
2020-07-25 10:41:03 -05:00
Moody Salem
32d300009e just bump the polling interval, aiming to have same # of blockNumber requests as calls or less 2020-07-21 15:57:09 -05:00
Moody Salem
806623c602 save some calls on the redundant chain id requests 2020-07-21 15:43:52 -05:00
Moody Salem
3272f8e9db chore(infura): rotate keys (complete) 2020-07-21 11:07:10 -05:00
Moody Salem
010ef108eb chore(infura): rotate keys 2020-07-21 11:05:55 -05:00
58 changed files with 2140 additions and 1041 deletions

2
.env
View File

@@ -1,2 +1,2 @@
REACT_APP_CHAIN_ID="1"
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/acb7e55995d04c49bfb52b7141599467"
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847"

View File

@@ -1,5 +1,5 @@
REACT_APP_CHAIN_ID="1"
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/febcb10ca2754433a61e0805bc6c047d"
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-4"

View File

@@ -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?:

View File

@@ -59,15 +59,15 @@ jobs:
with:
cidv0: ${{ steps.upload.outputs.hash }}
# - name: Update DNS with new IPFS hash
# env:
# CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
# RECORD_DOMAIN: 'uniswap.org'
# RECORD_NAME: '_dnslink.app'
# CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
# uses: textileio/cloudflare-update-dnslink@0fe7b7a1ffc865db3a4da9773f0f987447ad5848
# with:
# cid: ${{ steps.upload.outputs.hash }}
- name: Update DNS with new IPFS hash
env:
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
RECORD_DOMAIN: 'uniswap.org'
RECORD_NAME: '_dnslink.app'
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
uses: textileio/cloudflare-update-dnslink@0fe7b7a1ffc865db3a4da9773f0f987447ad5848
with:
cid: ${{ steps.upload.outputs.hash }}
- name: Create GitHub Release
id: create_release

View File

@@ -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

View File

@@ -73,9 +73,9 @@ Cypress.Commands.overwrite('visit', (original, url, options) => {
...options,
onBeforeLoad(win) {
options && options.onBeforeLoad && options.onBeforeLoad(win)
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/acb7e55995d04c49bfb52b7141599467', 4)
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 4)
const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, provider)
win.ethereum = new CustomizedBridge(signer, provider)
},
}
})
})

View File

@@ -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",

View File

@@ -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>
)
}

View 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 &quot;{oldList.name}&quot; has been updated to{' '}
<strong>{versionLabel(newList.version)}</strong>.
</TYPE.body>
) : (
<>
<div>
A token list update is available for the list &quot;{oldList.name}&quot; ({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>
)
}

View 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>
)
}

View 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>
)
}

View File

@@ -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>
)

View File

@@ -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 }} />

View File

@@ -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,

View File

@@ -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;

View File

@@ -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() ?? ''

View File

@@ -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>
)
}

View File

@@ -53,6 +53,9 @@ class MiniRpcProvider implements AsyncSendable {
if (typeof method !== 'string') {
return this.request(method.method, method.params)
}
if (method === 'eth_chainId') {
return `0x${this.chainId.toString(16)}`
}
const response = await fetch(this.url, {
method: 'POST',
body: JSON.stringify({

View File

@@ -6,7 +6,6 @@ import { PortisConnector } from '@web3-react/portis-connector'
import { FortmaticConnector } from './Fortmatic'
import { NetworkConnector } from './NetworkConnector'
const POLLING_INTERVAL = 10000
const NETWORK_URL = process.env.REACT_APP_NETWORK_URL
const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
@@ -28,7 +27,7 @@ export const walletconnect = new WalletConnectConnector({
rpc: { 1: NETWORK_URL },
bridge: 'https://bridge.walletconnect.org',
qrcode: true,
pollingInterval: POLLING_INTERVAL
pollingInterval: 15000
})
// mainnet only

View File

@@ -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'

View File

@@ -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]: {}
}
)

View File

@@ -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')
]

View File

@@ -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')
]

View File

@@ -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')
]

View File

@@ -1,3 +0,0 @@
import { Token, ChainId } from '@uniswap/sdk'
export default [new Token(ChainId.ROPSTEN, '0xaD6D458402F60fD3Bd25163575031ACDce07538D', 18, 'DAI', 'Dai Stablecoin')]

View File

@@ -123,14 +123,16 @@ export function useV1Trade(
pairs = [inputPair, outputPair]
}
const route = inputCurrency && pairs && pairs.length > 0 && new Route(pairs, inputCurrency)
const route = inputCurrency && pairs && pairs.length > 0 && new Route(pairs, inputCurrency, outputCurrency)
let v1Trade: Trade | undefined
try {
v1Trade =
route && exactAmount
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
: undefined
} catch {}
} catch (error) {
console.error('Failed to create V1 trade', error)
}
return v1Trade
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -12,6 +12,10 @@ import { useV1ExchangeContract } from './useContract'
import useENS from './useENS'
import { Version } from './useToggledVersion'
function isZero(hexNumber: string) {
return /^0x0*$/.test(hexNumber)
}
// returns a function that will execute a swap, if the parameters are all valid
// and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback(
@@ -76,7 +80,7 @@ export function useSwapCallback(
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
swapMethods.map(({ args, methodName, value }) =>
contract.estimateGas[methodName](...args, value ? { value } : {})
contract.estimateGas[methodName](...args, value && !isZero(value) ? { value } : {})
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed for ${methodName}`, error)
@@ -127,7 +131,7 @@ export function useSwapCallback(
return contract[methodName](...args, {
gasLimit: safeGasEstimate,
...(value ? { value } : {})
...(value && !isZero(value) ? { value } : {})
})
.then((response: any) => {
const inputSymbol = trade.inputAmount.currency.symbol

View File

@@ -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'

View File

@@ -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 />

View File

@@ -80,11 +80,11 @@ export default function App() {
<Route exact path="/add" component={AddLiquidity} />
<Route exact path="/add/:currencyIdA" component={RedirectOldAddLiquidityPathStructure} />
<Route exact path="/add/:currencyIdA/:currencyIdB" component={RedirectDuplicateTokenIds} />
<Route exact strict path="/remove/v1/:address" component={RemoveV1Exchange} />
<Route exact strict path="/remove/:tokens" component={RedirectOldRemoveLiquidityPathStructure} />
<Route exact strict path="/remove/:currencyIdA/:currencyIdB" component={RemoveLiquidity} />
<Route exact strict path="/migrate/v1" component={MigrateV1} />
<Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} />
<Route exact strict path="/remove/v1/:address" component={RemoveV1Exchange} />
<Route component={RedirectPathToSwapOnly} />
</Switch>
</Web3ReactManager>

View File

@@ -1,6 +1,6 @@
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { AddressZero } from '@ethersproject/constants'
import { Currency, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Currency, CurrencyAmount, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useMemo, useState } from 'react'
import ReactGA from 'react-ga'
import { Redirect, RouteComponentProps } from 'react-router'
@@ -28,21 +28,21 @@ import { getEtherscanLink, isAddress } from '../../utils'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
const POOL_CURRENCY_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
const ZERO = JSBI.BigInt(0)
const ONE = JSBI.BigInt(1)
const ZERO_FRACTION = new Fraction(ZERO, ONE)
const ALLOWED_OUTPUT_MIN_PERCENT = new Percent(JSBI.BigInt(10000 - INITIAL_ALLOWED_SLIPPAGE), JSBI.BigInt(10000))
function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
function FormattedPoolCurrencyAmount({ currencyAmount }: { currencyAmount: CurrencyAmount }) {
return (
<>
{tokenAmount.equalTo(JSBI.BigInt(0))
{currencyAmount.equalTo(JSBI.BigInt(0))
? '0'
: tokenAmount.greaterThan(POOL_TOKEN_AMOUNT_MIN)
? tokenAmount.toSignificant(4)
: `<${POOL_TOKEN_AMOUNT_MIN.toSignificant(1)}`}
: currencyAmount.greaterThan(POOL_CURRENCY_AMOUNT_MIN)
? currencyAmount.toSignificant(4)
: `<${POOL_CURRENCY_AMOUNT_MIN.toSignificant(1)}`}
</>
)
}
@@ -56,7 +56,7 @@ export function V1LiquidityInfo({
token: Token
liquidityTokenAmount: TokenAmount
tokenWorth: TokenAmount
ethWorth: Fraction
ethWorth: CurrencyAmount
}) {
const { chainId } = useActiveWeb3React()
@@ -66,7 +66,7 @@ export function V1LiquidityInfo({
<CurrencyLogo size="24px" currency={token} />
<div style={{ marginLeft: '.75rem' }}>
<TYPE.mediumHeader>
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />}{' '}
{<FormattedPoolCurrencyAmount currencyAmount={liquidityTokenAmount} />}{' '}
{token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH
</TYPE.mediumHeader>
</div>
@@ -89,7 +89,7 @@ export function V1LiquidityInfo({
</Text>
<RowFixed>
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{ethWorth.toSignificant(4)}
<FormattedPoolCurrencyAmount currencyAmount={ethWorth} />
</Text>
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={Currency.ETHER} />
</RowFixed>
@@ -114,9 +114,9 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
const ethWorth: Fraction = exchangeETHBalance
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
: ZERO_FRACTION
const ethWorth: CurrencyAmount = exchangeETHBalance
? CurrencyAmount.ether(exchangeETHBalance.multiply(shareFraction).multiply(WEI_DENOM).quotient)
: CurrencyAmount.ether(ZERO)
const tokenWorth: TokenAmount = exchangeTokenBalance
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)

View File

@@ -1,5 +1,5 @@
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { JSBI, Token, TokenAmount, WETH, Fraction, Percent } from '@uniswap/sdk'
import { JSBI, Token, TokenAmount, WETH, Fraction, Percent, CurrencyAmount } from '@uniswap/sdk'
import React, { useCallback, useMemo, useState } from 'react'
import ReactGA from 'react-ga'
import { Redirect, RouteComponentProps } from 'react-router'
@@ -49,9 +49,9 @@ function V1PairRemoval({
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
const ethWorth: Fraction = exchangeETHBalance
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
: ZERO_FRACTION
const ethWorth: CurrencyAmount = exchangeETHBalance
? CurrencyAmount.ether(exchangeETHBalance.multiply(shareFraction).multiply(WEI_DENOM).quotient)
: CurrencyAmount.ether(ZERO)
const tokenWorth: TokenAmount = exchangeTokenBalance
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)

View File

@@ -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()

View File

@@ -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')

View 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)
})
})
})

View File

@@ -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 => {

View File

@@ -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 })

View 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
View 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]
)
}

View 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
}
}
})
})
})
})

View 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
}
})
)

View 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
}

View File

@@ -1,4 +1,5 @@
import { createAction } from '@reduxjs/toolkit'
import { ChainId } from '@uniswap/sdk'
export interface SerializableTransactionReceipt {
to: string
@@ -12,15 +13,20 @@ export interface SerializableTransactionReceipt {
}
export const addTransaction = createAction<{
chainId: number
chainId: ChainId
hash: string
from: string
approval?: { tokenAddress: string; spender: string }
summary?: string
}>('addTransaction')
export const clearAllTransactions = createAction<{ chainId: number }>('clearAllTransactions')
}>('transactions/addTransaction')
export const clearAllTransactions = createAction<{ chainId: ChainId }>('transactions/clearAllTransactions')
export const finalizeTransaction = createAction<{
chainId: number
chainId: ChainId
hash: string
receipt: SerializableTransactionReceipt
}>('finalizeTransaction')
}>('transactions/finalizeTransaction')
export const checkedTransaction = createAction<{
chainId: ChainId
hash: string
blockNumber: number
}>('transactions/checkedTransaction')

View File

@@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks'
import { AppDispatch, AppState } from '../index'
import { addTransaction } from './actions'
import { TransactionDetails, TransactionState } from './reducer'
import { TransactionDetails } from './reducer'
// helper that can take a ethers library transaction response and add it to the list of transactions
export function useTransactionAdder(): (
@@ -37,7 +37,7 @@ export function useTransactionAdder(): (
export function useAllTransactions(): { [txHash: string]: TransactionDetails } {
const { chainId } = useActiveWeb3React()
const state = useSelector<AppState, TransactionState>(state => state.transactions)
const state = useSelector<AppState, AppState['transactions']>(state => state.transactions)
return chainId ? state[chainId] ?? {} : {}
}

View File

@@ -0,0 +1,192 @@
import { ChainId } from '@uniswap/sdk'
import { createStore, Store } from 'redux'
import { addTransaction, checkedTransaction, clearAllTransactions, finalizeTransaction } from './actions'
import reducer, { initialState, TransactionState } from './reducer'
describe('transaction reducer', () => {
let store: Store<TransactionState>
beforeEach(() => {
store = createStore(reducer, initialState)
})
describe('addTransaction', () => {
it('adds the transaction', () => {
const beforeTime = new Date().getTime()
store.dispatch(
addTransaction({
chainId: ChainId.MAINNET,
summary: 'hello world',
hash: '0x0',
approval: { tokenAddress: 'abc', spender: 'def' },
from: 'abc'
})
)
const txs = store.getState()
expect(txs[ChainId.MAINNET]).toBeTruthy()
expect(txs[ChainId.MAINNET]?.['0x0']).toBeTruthy()
const tx = txs[ChainId.MAINNET]?.['0x0']
expect(tx).toBeTruthy()
expect(tx?.hash).toEqual('0x0')
expect(tx?.summary).toEqual('hello world')
expect(tx?.approval).toEqual({ tokenAddress: 'abc', spender: 'def' })
expect(tx?.from).toEqual('abc')
expect(tx?.addedTime).toBeGreaterThanOrEqual(beforeTime)
})
})
describe('finalizeTransaction', () => {
it('no op if not valid transaction', () => {
store.dispatch(
finalizeTransaction({
chainId: ChainId.RINKEBY,
hash: '0x0',
receipt: {
status: 1,
transactionIndex: 1,
transactionHash: '0x0',
to: '0x0',
from: '0x0',
contractAddress: '0x0',
blockHash: '0x0',
blockNumber: 1
}
})
)
expect(store.getState()).toEqual({})
})
it('sets receipt', () => {
store.dispatch(
addTransaction({
hash: '0x0',
chainId: ChainId.RINKEBY,
approval: { spender: '0x0', tokenAddress: '0x0' },
summary: 'hello world',
from: '0x0'
})
)
const beforeTime = new Date().getTime()
store.dispatch(
finalizeTransaction({
chainId: ChainId.RINKEBY,
hash: '0x0',
receipt: {
status: 1,
transactionIndex: 1,
transactionHash: '0x0',
to: '0x0',
from: '0x0',
contractAddress: '0x0',
blockHash: '0x0',
blockNumber: 1
}
})
)
const tx = store.getState()[ChainId.RINKEBY]?.['0x0']
expect(tx?.summary).toEqual('hello world')
expect(tx?.confirmedTime).toBeGreaterThanOrEqual(beforeTime)
expect(tx?.receipt).toEqual({
status: 1,
transactionIndex: 1,
transactionHash: '0x0',
to: '0x0',
from: '0x0',
contractAddress: '0x0',
blockHash: '0x0',
blockNumber: 1
})
})
})
describe('checkedTransaction', () => {
it('no op if not valid transaction', () => {
store.dispatch(
checkedTransaction({
chainId: ChainId.RINKEBY,
hash: '0x0',
blockNumber: 1
})
)
expect(store.getState()).toEqual({})
})
it('sets lastCheckedBlockNumber', () => {
store.dispatch(
addTransaction({
hash: '0x0',
chainId: ChainId.RINKEBY,
approval: { spender: '0x0', tokenAddress: '0x0' },
summary: 'hello world',
from: '0x0'
})
)
store.dispatch(
checkedTransaction({
chainId: ChainId.RINKEBY,
hash: '0x0',
blockNumber: 1
})
)
const tx = store.getState()[ChainId.RINKEBY]?.['0x0']
expect(tx?.lastCheckedBlockNumber).toEqual(1)
})
it('never decreases', () => {
store.dispatch(
addTransaction({
hash: '0x0',
chainId: ChainId.RINKEBY,
approval: { spender: '0x0', tokenAddress: '0x0' },
summary: 'hello world',
from: '0x0'
})
)
store.dispatch(
checkedTransaction({
chainId: ChainId.RINKEBY,
hash: '0x0',
blockNumber: 3
})
)
store.dispatch(
checkedTransaction({
chainId: ChainId.RINKEBY,
hash: '0x0',
blockNumber: 1
})
)
const tx = store.getState()[ChainId.RINKEBY]?.['0x0']
expect(tx?.lastCheckedBlockNumber).toEqual(3)
})
})
describe('clearAllTransactions', () => {
it('removes all transactions for the chain', () => {
store.dispatch(
addTransaction({
chainId: ChainId.MAINNET,
summary: 'hello world',
hash: '0x0',
approval: { tokenAddress: 'abc', spender: 'def' },
from: 'abc'
})
)
store.dispatch(
addTransaction({
chainId: ChainId.RINKEBY,
summary: 'hello world',
hash: '0x1',
approval: { tokenAddress: 'abc', spender: 'def' },
from: 'abc'
})
)
expect(Object.keys(store.getState())).toHaveLength(2)
expect(Object.keys(store.getState())).toEqual([String(ChainId.MAINNET), String(ChainId.RINKEBY)])
expect(Object.keys(store.getState()[ChainId.MAINNET] ?? {})).toEqual(['0x0'])
expect(Object.keys(store.getState()[ChainId.RINKEBY] ?? {})).toEqual(['0x1'])
store.dispatch(clearAllTransactions({ chainId: ChainId.MAINNET }))
expect(Object.keys(store.getState())).toHaveLength(2)
expect(Object.keys(store.getState())).toEqual([String(ChainId.MAINNET), String(ChainId.RINKEBY)])
expect(Object.keys(store.getState()[ChainId.MAINNET] ?? {})).toEqual([])
expect(Object.keys(store.getState()[ChainId.RINKEBY] ?? {})).toEqual(['0x1'])
})
})
})

View File

@@ -1,5 +1,11 @@
import { createReducer } from '@reduxjs/toolkit'
import { addTransaction, clearAllTransactions, finalizeTransaction, SerializableTransactionReceipt } from './actions'
import {
addTransaction,
checkedTransaction,
clearAllTransactions,
finalizeTransaction,
SerializableTransactionReceipt
} from './actions'
const now = () => new Date().getTime()
@@ -8,12 +14,10 @@ export interface TransactionDetails {
approval?: { tokenAddress: string; spender: string }
summary?: string
receipt?: SerializableTransactionReceipt
lastCheckedBlockNumber?: number
addedTime: number
confirmedTime?: number
from: string
// set to true when we receive a transaction count that exceeds the nonce of this transaction
unknownStatus?: boolean
}
export interface TransactionState {
@@ -22,28 +26,39 @@ export interface TransactionState {
}
}
const initialState: TransactionState = {}
export const initialState: TransactionState = {}
export default createReducer(initialState, builder =>
builder
.addCase(addTransaction, (state, { payload: { chainId, from, hash, approval, summary } }) => {
if (state[chainId]?.[hash]) {
.addCase(addTransaction, (transactions, { payload: { chainId, from, hash, approval, summary } }) => {
if (transactions[chainId]?.[hash]) {
throw Error('Attempted to add existing transaction.')
}
state[chainId] = state[chainId] ?? {}
state[chainId][hash] = { hash, approval, summary, from, addedTime: now() }
const txs = transactions[chainId] ?? {}
txs[hash] = { hash, approval, summary, from, addedTime: now() }
transactions[chainId] = txs
})
.addCase(clearAllTransactions, (state, { payload: { chainId } }) => {
if (!state[chainId]) return
state[chainId] = {}
.addCase(clearAllTransactions, (transactions, { payload: { chainId } }) => {
if (!transactions[chainId]) return
transactions[chainId] = {}
})
.addCase(finalizeTransaction, (state, { payload: { hash, chainId, receipt } }) => {
if (!state[chainId]?.[hash]) {
throw Error('Attempted to finalize non-existent transaction.')
.addCase(checkedTransaction, (transactions, { payload: { chainId, hash, blockNumber } }) => {
const tx = transactions[chainId]?.[hash]
if (!tx) {
return
}
state[chainId] = state[chainId] ?? {}
state[chainId][hash].receipt = receipt
state[chainId][hash].unknownStatus = false
state[chainId][hash].confirmedTime = now()
if (!tx.lastCheckedBlockNumber) {
tx.lastCheckedBlockNumber = blockNumber
} else {
tx.lastCheckedBlockNumber = Math.max(blockNumber, tx.lastCheckedBlockNumber)
}
})
.addCase(finalizeTransaction, (transactions, { payload: { hash, chainId, receipt } }) => {
const tx = transactions[chainId]?.[hash]
if (!tx) {
return
}
tx.receipt = receipt
tx.confirmedTime = now()
})
)

View File

@@ -0,0 +1,35 @@
import { shouldCheck } from './updater'
describe('transactions updater', () => {
describe('shouldCheck', () => {
it('returns true if no receipt and never checked', () => {
expect(shouldCheck(10, { addedTime: 100 })).toEqual(true)
})
it('returns false if has receipt and never checked', () => {
expect(shouldCheck(10, { addedTime: 100, receipt: {} })).toEqual(false)
})
it('returns true if has not been checked in 1 blocks', () => {
expect(shouldCheck(10, { addedTime: new Date().getTime(), lastCheckedBlockNumber: 9 })).toEqual(true)
})
it('returns false if checked in last 3 blocks and greater than 20 minutes old', () => {
expect(shouldCheck(10, { addedTime: new Date().getTime() - 21 * 60 * 1000, lastCheckedBlockNumber: 8 })).toEqual(
false
)
})
it('returns true if not checked in last 5 blocks and greater than 20 minutes old', () => {
expect(shouldCheck(10, { addedTime: new Date().getTime() - 21 * 60 * 1000, lastCheckedBlockNumber: 5 })).toEqual(
true
)
})
it('returns false if checked in last 10 blocks and greater than 60 minutes old', () => {
expect(shouldCheck(20, { addedTime: new Date().getTime() - 61 * 60 * 1000, lastCheckedBlockNumber: 11 })).toEqual(
false
)
})
it('returns true if checked in last 3 blocks and greater than 20 minutes old', () => {
expect(shouldCheck(20, { addedTime: new Date().getTime() - 61 * 60 * 1000, lastCheckedBlockNumber: 10 })).toEqual(
true
)
})
})
})

View File

@@ -3,7 +3,28 @@ import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks'
import { useAddPopup, useBlockNumber } from '../application/hooks'
import { AppDispatch, AppState } from '../index'
import { finalizeTransaction } from './actions'
import { checkedTransaction, finalizeTransaction } from './actions'
export function shouldCheck(
lastBlockNumber: number,
tx: { addedTime: number; receipt?: {}; lastCheckedBlockNumber?: number }
): boolean {
if (tx.receipt) return false
if (!tx.lastCheckedBlockNumber) return true
const blocksSinceCheck = lastBlockNumber - tx.lastCheckedBlockNumber
if (blocksSinceCheck < 1) return false
const minutesPending = (new Date().getTime() - tx.addedTime) / 1000 / 60
if (minutesPending > 60) {
// every 10 blocks if pending for longer than an hour
return blocksSinceCheck > 9
} else if (minutesPending > 5) {
// every 3 blocks if pending more than 5 minutes
return blocksSinceCheck > 2
} else {
// otherwise every block
return true
}
}
export default function Updater() {
const { chainId, library } = useActiveWeb3React()
@@ -11,9 +32,9 @@ export default function Updater() {
const lastBlockNumber = useBlockNumber()
const dispatch = useDispatch<AppDispatch>()
const transactions = useSelector<AppState, AppState['transactions']>(state => state.transactions)
const state = useSelector<AppState, AppState['transactions']>(state => state.transactions)
const allTransactions = transactions[chainId ?? -1] ?? {}
const transactions = chainId ? state[chainId] ?? {} : {}
// show popup on confirm
const addPopup = useAddPopup()
@@ -21,8 +42,8 @@ export default function Updater() {
useEffect(() => {
if (!chainId || !library || !lastBlockNumber) return
Object.keys(allTransactions)
.filter(hash => !allTransactions[hash].receipt)
Object.keys(transactions)
.filter(hash => shouldCheck(lastBlockNumber, transactions[hash]))
.forEach(hash => {
library
.getTransactionReceipt(hash)
@@ -50,18 +71,20 @@ export default function Updater() {
txn: {
hash,
success: receipt.status === 1,
summary: allTransactions[hash]?.summary
summary: transactions[hash]?.summary
}
},
hash
)
} else {
dispatch(checkedTransaction({ chainId, hash, blockNumber: lastBlockNumber }))
}
})
.catch(error => {
console.error(`failed to check transaction hash: ${hash}`, error)
})
})
}, [chainId, library, allTransactions, lastBlockNumber, dispatch, addPopup])
}, [chainId, library, transactions, lastBlockNumber, dispatch, addPopup])
return null
}

View File

@@ -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]

View File

@@ -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)

View File

@@ -175,6 +175,7 @@ html, input, textarea, button {
@supports (font-variation-settings: normal) {
html, input, textarea, button {
font-family: 'Inter var', sans-serif;
font-display: fallback;
}
}

View File

@@ -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
View 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 []
}
}

View File

@@ -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'

1255
yarn.lock

File diff suppressed because it is too large Load Diff