feat(lists): allow selecting and adding token lists (#1023)
* more list stuff Use the selected list instead of the default list, but also use the default list start list selection code * move token warning to a modal, fix the install issue * add/remove/enter key * handle enter on currency select for ETHER * change slippage tolerance to be a slider * make ui closer to the mocks * commit slider changes * back to tabs * copy changes * bump list version * some styling for the list select * bump uniswap default list version * use contract calls to get ens names and addresses * show list logo * fix failing integration test * .eth.link * list introduction screen * remove showSendWithSwap * fix integration and unit tests * resolve ENS names * logos from ens * fix the lint errors * some refactoring to better support using a the library provider from the user for resolving ENS names * load list info from the list url for the introduction page * make it slightly harder to remove a list * minor clean up, some help text and links * remove icon from list update popup * show added/removed tokens * add GA everywhere, don't debounce contenthash lookups * show tags * fix tag key * tag display, list rendering, needs optimization * fix list fetching in firefox, style issue in safari * sort the lists, clean up styling * use client provider when possible * show token warning for url loaded tokens * improve the warning modal * some refactoring to fix the list fetching on networks other than mainnet * fix tests * some minor improvements * increase timeout to maybe fix integration tests which pass locally * build for tests using the dev network url * reset the lists if we deleted the other two copies * improve how we handle updating the default list of lists * fix integration test * Update token list selection styles * fix external links, reuse the on click outside code, show add errors * show the list origin instead of the full url * fix update list link * show host instead of hostname do not automatically dismiss major version upgrades for lists * fix link to tokenlists.org * add uma * clean up styling in list rows * bump token list version * bump token list version again * hover symbol to see currency name * bump version * add cmc lists, dharma list Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>
This commit is contained in:
parent
09b54570e1
commit
7cf25ac7c8
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
@ -29,6 +29,8 @@ jobs:
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn cypress install
|
||||
- run: yarn build
|
||||
env:
|
||||
REACT_APP_NETWORK_URL: "https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847"
|
||||
- run: yarn integration-test
|
||||
|
||||
unit-tests:
|
||||
|
@ -3,5 +3,6 @@
|
||||
"pluginsFile": false,
|
||||
"fixturesFolder": false,
|
||||
"supportFile": "cypress/support/index.js",
|
||||
"video": false
|
||||
"video": false,
|
||||
"defaultCommandTimeout": 10000
|
||||
}
|
||||
|
11
cypress/integration/lists.test.ts
Normal file
11
cypress/integration/lists.test.ts
Normal file
@ -0,0 +1,11 @@
|
||||
describe('Swap', () => {
|
||||
beforeEach(() => cy.visit('/swap'))
|
||||
|
||||
it('list selection persists', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#select-default-uniswap-list .select-button').click()
|
||||
cy.reload()
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#select-default-uniswap-list').should('not.exist')
|
||||
})
|
||||
})
|
@ -1,5 +1,8 @@
|
||||
describe('Swap', () => {
|
||||
beforeEach(() => cy.visit('/swap'))
|
||||
beforeEach(() => {
|
||||
cy.clearLocalStorage()
|
||||
cy.visit('/swap')
|
||||
})
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.001', { delay: 200 })
|
||||
@ -32,6 +35,7 @@ describe('Swap', () => {
|
||||
|
||||
it('can swap ETH for DAI', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#select-default-uniswap-list .select-button').click()
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').should('be.visible')
|
||||
|
@ -6,14 +6,11 @@ describe('Warning', () => {
|
||||
it('Check that warning is displayed', () => {
|
||||
cy.get('.token-warning-container').should('be.visible')
|
||||
})
|
||||
it('Check that warning hides after button dismissal.', () => {
|
||||
it('Check that warning hides after button dismissal', () => {
|
||||
cy.get('.token-dismiss-button').should('be.disabled')
|
||||
cy.get('.understand-checkbox').click()
|
||||
cy.get('.token-dismiss-button').should('not.be.disabled')
|
||||
cy.get('.token-dismiss-button').click()
|
||||
cy.get('.token-warning-container').should('not.be.visible')
|
||||
})
|
||||
it('Check supression persists across sessions.', () => {
|
||||
cy.get('.token-warning-container').should('be.visible')
|
||||
cy.get('.token-dismiss-button').click()
|
||||
cy.reload()
|
||||
cy.get('.token-warning-container').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
@ -11,20 +11,23 @@
|
||||
"@reduxjs/toolkit": "^1.3.5",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/lodash.flatmap": "^4.5.6",
|
||||
"@types/multicodec": "^1.0.0",
|
||||
"@types/node": "^13.13.5",
|
||||
"@types/qs": "^6.9.2",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-dom": "^16.9.7",
|
||||
"@types/react-redux": "^7.1.8",
|
||||
"@types/react-router-dom": "^5.0.0",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.0",
|
||||
"@types/react-window": "^1.8.2",
|
||||
"@types/rebass": "^4.0.5",
|
||||
"@types/styled-components": "^5.1.0",
|
||||
"@types/testing-library__cypress": "^5.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^2.31.0",
|
||||
"@uniswap/default-token-list": "^1.3.0",
|
||||
"@uniswap/sdk": "3.0.3-beta.1",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.11",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.14",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@web3-react/core": "^6.0.9",
|
||||
@ -34,6 +37,7 @@
|
||||
"@web3-react/walletconnect-connector": "^6.1.1",
|
||||
"@web3-react/walletlink-connector": "^6.0.9",
|
||||
"ajv": "^6.12.3",
|
||||
"cids": "^1.0.0",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"cypress": "^4.11.0",
|
||||
@ -49,6 +53,8 @@
|
||||
"inter-ui": "^3.13.1",
|
||||
"jazzicon": "^1.5.0",
|
||||
"lodash.flatmap": "^4.5.0",
|
||||
"multicodec": "^2.0.0",
|
||||
"multihashes": "^3.0.1",
|
||||
"polished": "^3.3.2",
|
||||
"prettier": "^1.17.0",
|
||||
"qs": "^6.9.4",
|
||||
@ -64,6 +70,7 @@
|
||||
"react-scripts": "^3.4.1",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.5",
|
||||
"rebass": "^4.0.7",
|
||||
"redux-localstorage-simple": "^2.2.0",
|
||||
|
@ -19,11 +19,11 @@ export const LightCard = styled(Card)`
|
||||
`
|
||||
|
||||
export const GreyCard = styled(Card)`
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
export const OutlineCard = styled(Card)`
|
||||
border: 1px solid ${({ theme }) => theme.advancedBG};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
export const YellowCard = styled(Card)`
|
||||
|
@ -126,7 +126,6 @@ interface CurrencyInputPanelProps {
|
||||
hideBalance?: boolean
|
||||
pair?: Pair | null
|
||||
hideInput?: boolean
|
||||
showSendWithSwap?: boolean
|
||||
otherCurrency?: Currency | null
|
||||
id: string
|
||||
showCommonBases?: boolean
|
||||
@ -144,7 +143,6 @@ export default function CurrencyInputPanel({
|
||||
hideBalance = false,
|
||||
pair = null, // used for double token logo
|
||||
hideInput = false,
|
||||
showSendWithSwap = false,
|
||||
otherCurrency = null,
|
||||
id,
|
||||
showCommonBases
|
||||
@ -238,8 +236,7 @@ export default function CurrencyInputPanel({
|
||||
isOpen={modalOpen}
|
||||
onDismiss={handleDismissSearch}
|
||||
onCurrencySelect={onCurrencySelect}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
hiddenCurrency={currency}
|
||||
selectedCurrency={currency}
|
||||
otherSelectedCurrency={otherCurrency}
|
||||
showCommonBases={showCommonBases}
|
||||
/>
|
||||
|
@ -1,32 +1,14 @@
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import EthereumLogo from '../../assets/images/ethereum-logo.png'
|
||||
import useHttpLocations from '../../hooks/useHttpLocations'
|
||||
import { WrappedTokenInfo } from '../../state/lists/hooks'
|
||||
import uriToHttp from '../../utils/uriToHttp'
|
||||
import Logo from '../Logo'
|
||||
|
||||
const getTokenLogoURL = address =>
|
||||
const getTokenLogoURL = (address: string) =>
|
||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||
const BAD_URIS: { [tokenAddress: string]: true } = {}
|
||||
|
||||
const Image = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
background-color: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
|
||||
`
|
||||
|
||||
const Emoji = styled.span<{ size?: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
margin-bottom: -4px;
|
||||
`
|
||||
|
||||
const StyledEthereumLogo = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
@ -35,60 +17,38 @@ const StyledEthereumLogo = styled.img<{ size: string }>`
|
||||
border-radius: 24px;
|
||||
`
|
||||
|
||||
const StyledLogo = styled(Logo)<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
`
|
||||
|
||||
export default function CurrencyLogo({
|
||||
currency,
|
||||
size = '24px',
|
||||
...rest
|
||||
style
|
||||
}: {
|
||||
currency?: Currency
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
const [, refresh] = useState<number>(0)
|
||||
const uriLocations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined)
|
||||
|
||||
const srcs: string[] = useMemo(() => {
|
||||
if (currency === ETHER) return []
|
||||
|
||||
if (currency instanceof Token) {
|
||||
if (currency instanceof WrappedTokenInfo) {
|
||||
return [...uriLocations, getTokenLogoURL(currency.address)]
|
||||
}
|
||||
|
||||
return [getTokenLogoURL(currency.address)]
|
||||
}
|
||||
return []
|
||||
}, [currency, uriLocations])
|
||||
|
||||
if (currency === ETHER) {
|
||||
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
|
||||
return <StyledEthereumLogo src={EthereumLogo} size={size} style={style} />
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
return <StyledLogo size={size} srcs={srcs} alt={`${currency?.symbol ?? 'token'} logo`} style={style} />
|
||||
}
|
||||
|
26
src/components/ListLogo/index.tsx
Normal file
26
src/components/ListLogo/index.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import useHttpLocations from '../../hooks/useHttpLocations'
|
||||
|
||||
import Logo from '../Logo'
|
||||
|
||||
const StyledListLogo = styled(Logo)<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
`
|
||||
|
||||
export default function ListLogo({
|
||||
logoURI,
|
||||
style,
|
||||
size = '24px',
|
||||
alt
|
||||
}: {
|
||||
logoURI: string
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
alt?: string
|
||||
}) {
|
||||
const srcs: string[] = useHttpLocations(logoURI)
|
||||
|
||||
return <StyledListLogo alt={alt} size={size} srcs={srcs} style={style} />
|
||||
}
|
34
src/components/Logo/index.tsx
Normal file
34
src/components/Logo/index.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React, { useState } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { ImageProps } from 'rebass'
|
||||
|
||||
const BAD_SRCS: { [tokenAddress: string]: true } = {}
|
||||
|
||||
export interface LogoProps extends Pick<ImageProps, 'style' | 'alt' | 'className'> {
|
||||
srcs: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an image by sequentially trying a list of URIs, and then eventually a fallback triangle alert
|
||||
*/
|
||||
export default function Logo({ srcs, alt, ...rest }: LogoProps) {
|
||||
const [, refresh] = useState<number>(0)
|
||||
|
||||
const src: string | undefined = srcs.find(src => !BAD_SRCS[src])
|
||||
|
||||
if (src) {
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
alt={alt}
|
||||
src={src}
|
||||
onError={() => {
|
||||
if (src) BAD_SRCS[src] = true
|
||||
refresh(i => i + 1)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <AlertTriangle {...rest} />
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import { Info, BookOpen, Code, PieChart, MessageCircle } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
|
||||
import { ExternalLink } from '../../theme'
|
||||
@ -83,24 +84,7 @@ export default function Menu() {
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const [open, toggle] = useToggle(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = e => {
|
||||
if (node.current?.contains(e.target) ?? false) {
|
||||
return
|
||||
}
|
||||
toggle()
|
||||
}
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [open, toggle])
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
|
@ -38,6 +38,7 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadow1)};
|
||||
padding: 0px;
|
||||
width: 50vw;
|
||||
overflow: hidden;
|
||||
|
||||
align-self: ${({ mobile }) => (mobile ? 'flex-end' : 'center')};
|
||||
|
||||
|
@ -1,20 +1,17 @@
|
||||
import { TokenList, Version } from '@uniswap/token-lists'
|
||||
import React, { useCallback, useContext } from 'react'
|
||||
import { AlertCircle, Info } from 'react-feather'
|
||||
import { diffTokenLists, TokenList } from '@uniswap/token-lists'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Text } from 'rebass'
|
||||
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 listVersionLabel from '../../utils/listVersionLabel'
|
||||
import { 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,
|
||||
@ -31,34 +28,68 @@ export default function ListUpdatePopup({
|
||||
const removePopup = useRemovePopup()
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const updateList = useCallback(() => {
|
||||
const handleAcceptUpdate = useCallback(() => {
|
||||
if (auto) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Update List from Popup',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
removeThisPopup()
|
||||
}, [auto, dispatch, listUrl, removeThisPopup])
|
||||
|
||||
const { added: tokensAdded, changed: tokensChanged, removed: tokensRemoved } = useMemo(() => {
|
||||
return diffTokenLists(oldList.tokens, newList.tokens)
|
||||
}, [newList.tokens, oldList.tokens])
|
||||
const numTokensChanged = useMemo(
|
||||
() => Object.keys(tokensChanged).reduce((memo, chainId) => memo + Object.keys(tokensChanged[chainId]).length, 0),
|
||||
[tokensChanged]
|
||||
)
|
||||
|
||||
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>.
|
||||
<strong>{listVersionLabel(newList.version)}</strong>.
|
||||
</TYPE.body>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
A token list update is available for the list "{oldList.name}" ({versionLabel(oldList.version)}{' '}
|
||||
to {versionLabel(newList.version)}).
|
||||
<Text>
|
||||
An update is available for the token list "{oldList.name}" (
|
||||
{listVersionLabel(oldList.version)} to {listVersionLabel(newList.version)}).
|
||||
</Text>
|
||||
<ul>
|
||||
{tokensAdded.length > 0 ? (
|
||||
<li>
|
||||
{tokensAdded.map(token => (
|
||||
<strong key={`${token.chainId}-${token.address}`} title={token.address}>
|
||||
{token.symbol}
|
||||
</strong>
|
||||
))}{' '}
|
||||
added
|
||||
</li>
|
||||
) : null}
|
||||
{tokensRemoved.length > 0 ? (
|
||||
<li>
|
||||
{tokensRemoved.map(token => (
|
||||
<strong key={`${token.chainId}-${token.address}`} title={token.address}>
|
||||
{token.symbol}
|
||||
</strong>
|
||||
))}{' '}
|
||||
removed
|
||||
</li>
|
||||
) : null}
|
||||
{numTokensChanged > 0 ? <li>{numTokensChanged} tokens updated</li> : null}
|
||||
</ul>
|
||||
</div>
|
||||
<AutoRow>
|
||||
<div style={{ flexGrow: 1, marginRight: 6 }}>
|
||||
<ButtonPrimary onClick={updateList}>Update list</ButtonPrimary>
|
||||
<div style={{ flexGrow: 1, marginRight: 12 }}>
|
||||
<ButtonSecondary onClick={handleAcceptUpdate}>Accept update</ButtonSecondary>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<ButtonSecondary onClick={removeThisPopup}>Dismiss</ButtonSecondary>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useCallback, useContext, useState } from 'react'
|
||||
import React, { useCallback, useContext, useEffect } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { useSpring } from 'react-spring/web'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import { animated } from 'react-spring'
|
||||
import { PopupContent } from '../../state/application/actions'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import ListUpdatePopup from './ListUpdatePopup'
|
||||
@ -25,44 +26,48 @@ export const Popup = styled.div`
|
||||
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 }>`
|
||||
const Fader = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: ${({ count }) => `calc(100% - (100% / ${150 / count}))`};
|
||||
width: 100%;
|
||||
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 AnimatedFader = animated(Fader)
|
||||
|
||||
const [isRunning, setIsRunning] = useState(true)
|
||||
export default function PopupItem({
|
||||
removeAfterMs,
|
||||
content,
|
||||
popKey
|
||||
}: {
|
||||
removeAfterMs: number | null
|
||||
content: PopupContent
|
||||
popKey: string
|
||||
}) {
|
||||
const removePopup = useRemovePopup()
|
||||
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
useEffect(() => {
|
||||
if (removeAfterMs === null) return
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
count > 150 ? removeThisPopup() : setCount(count + 1)
|
||||
},
|
||||
isRunning ? DELAY : null
|
||||
)
|
||||
const timeout = setTimeout(() => {
|
||||
removeThisPopup()
|
||||
}, removeAfterMs)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [removeAfterMs, removeThisPopup])
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
|
||||
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
|
||||
|
||||
let popupContent
|
||||
if ('txn' in content) {
|
||||
const {
|
||||
@ -76,11 +81,13 @@ export default function PopupItem({ content, popKey }: { content: PopupContent;
|
||||
popupContent = <ListUpdatePopup popKey={popKey} listUrl={listUrl} oldList={oldList} newList={newList} auto={auto} />
|
||||
}
|
||||
|
||||
const faderStyle = useSpring({ from: { width: '100%' }, to: { width: '0%' }, config: { duration: removeAfterMs } })
|
||||
|
||||
return (
|
||||
<Popup onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<StyledClose color={theme.text2} onClick={() => removePopup(popKey)} />
|
||||
<Popup>
|
||||
<StyledClose color={theme.text2} onClick={removeThisPopup} />
|
||||
{popupContent}
|
||||
<Fader count={count} />
|
||||
{removeAfterMs !== null ? <AnimatedFader style={faderStyle} /> : null}
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ const FixedPopupColumn = styled(AutoColumn)`
|
||||
right: 1rem;
|
||||
max-width: 355px !important;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: none;
|
||||
@ -49,7 +50,7 @@ export default function Popups() {
|
||||
<>
|
||||
<FixedPopupColumn gap="20px">
|
||||
{activePopups.map(item => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} />
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
</FixedPopupColumn>
|
||||
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
|
||||
@ -58,7 +59,7 @@ export default function Popups() {
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map(item => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} />
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
</MobilePopupInner>
|
||||
</MobilePopupWrapper>
|
||||
|
@ -22,7 +22,7 @@ const QuestionWrapper = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export default function QuestionHelper({ text, disabled }: { text: string; disabled?: boolean }) {
|
||||
export default function QuestionHelper({ text }: { text: string }) {
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
@ -30,7 +30,7 @@ export default function QuestionHelper({ text, disabled }: { text: string; disab
|
||||
|
||||
return (
|
||||
<span style={{ marginLeft: 4 }}>
|
||||
<Tooltip text={text} show={show && !disabled}>
|
||||
<Tooltip text={text} show={show}>
|
||||
<QuestionWrapper onClick={open} onMouseEnter={open} onMouseLeave={close}>
|
||||
<Question size={16} />
|
||||
</QuestionWrapper>
|
||||
|
@ -44,7 +44,11 @@ export default function CommonBases({
|
||||
</AutoRow>
|
||||
<AutoRow gap="4px">
|
||||
<BaseWrapper
|
||||
onClick={() => !currencyEquals(selectedCurrency, ETHER) && onSelect(ETHER)}
|
||||
onClick={() => {
|
||||
if (!selectedCurrency || !currencyEquals(selectedCurrency, ETHER)) {
|
||||
onSelect(ETHER)
|
||||
}
|
||||
}}
|
||||
disable={selectedCurrency === ETHER}
|
||||
>
|
||||
<CurrencyLogo currency={ETHER} style={{ marginRight: 8 }} />
|
||||
|
@ -1,74 +1,120 @@
|
||||
import { Currency, CurrencyAmount, currencyEquals, ETHER, JSBI, Token } from '@uniswap/sdk'
|
||||
import React, { CSSProperties, memo, useContext, useMemo } from 'react'
|
||||
import { Currency, CurrencyAmount, currencyEquals, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { CSSProperties, MutableRefObject, useCallback, useMemo } from 'react'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useDefaultTokenList } from '../../state/lists/hooks'
|
||||
import { useSelectedTokenList, WrappedTokenInfo } from '../../state/lists/hooks'
|
||||
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { useETHBalances } from '../../state/wallet/hooks'
|
||||
import { useCurrencyBalance } from '../../state/wallet/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import Column from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
import { FadedSpan, MenuItem } from './styleds'
|
||||
import Loader from '../Loader'
|
||||
import { isDefaultToken } from '../../utils'
|
||||
import { isTokenOnList } from '../../utils'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : ''
|
||||
}
|
||||
|
||||
export default function CurrencyList({
|
||||
currencies,
|
||||
allBalances,
|
||||
selectedCurrency,
|
||||
onCurrencySelect,
|
||||
otherCurrency,
|
||||
showSendWithSwap
|
||||
const StyledBalanceText = styled(Text)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 5rem;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const Tag = styled.div`
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem 0.3rem 0.25rem 0.3rem;
|
||||
max-width: 6rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
justify-self: flex-end;
|
||||
margin-right: 4px;
|
||||
`
|
||||
|
||||
function Balance({ balance }: { balance: CurrencyAmount }) {
|
||||
return <StyledBalanceText title={balance.toExact()}>{balance.toSignificant(4)}</StyledBalanceText>
|
||||
}
|
||||
|
||||
const TagContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
function TokenTags({ currency }: { currency: Currency }) {
|
||||
if (!(currency instanceof WrappedTokenInfo)) {
|
||||
return <span />
|
||||
}
|
||||
|
||||
const tags = currency.tags
|
||||
if (!tags || tags.length === 0) return <span />
|
||||
|
||||
const tag = tags[0]
|
||||
|
||||
return (
|
||||
<TagContainer>
|
||||
<MouseoverTooltip text={tag.description}>
|
||||
<Tag key={tag.id}>{tag.name}</Tag>
|
||||
</MouseoverTooltip>
|
||||
{tags.length > 1 ? (
|
||||
<MouseoverTooltip
|
||||
text={tags
|
||||
.slice(1)
|
||||
.map(({ name, description }) => `${name}: ${description}`)
|
||||
.join('; \n')}
|
||||
>
|
||||
<Tag>...</Tag>
|
||||
</MouseoverTooltip>
|
||||
) : null}
|
||||
</TagContainer>
|
||||
)
|
||||
}
|
||||
|
||||
function CurrencyRow({
|
||||
currency,
|
||||
onSelect,
|
||||
isSelected,
|
||||
otherSelected,
|
||||
style
|
||||
}: {
|
||||
currencies: Currency[]
|
||||
selectedCurrency: Currency
|
||||
allBalances: { [tokenAddress: string]: CurrencyAmount }
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherCurrency: Currency
|
||||
showSendWithSwap?: boolean
|
||||
currency: Currency
|
||||
onSelect: () => void
|
||||
isSelected: boolean
|
||||
otherSelected: boolean
|
||||
style: CSSProperties
|
||||
}) {
|
||||
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]
|
||||
|
||||
const CurrencyRow = useMemo(() => {
|
||||
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(defaultTokens, currency)
|
||||
const customAdded = Boolean(!isDefault && currency instanceof Token && allTokens[currency.address])
|
||||
const balance = currency === ETHER ? ETHBalance : allBalances[key]
|
||||
const selectedTokenList = useSelectedTokenList()
|
||||
const isOnSelectedList = isTokenOnList(selectedTokenList, currency)
|
||||
const customAdded = Boolean(!isOnSelectedList && currency instanceof Token)
|
||||
const balance = useCurrencyBalance(account ?? undefined, currency)
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
|
||||
const isSelected = Boolean(selectedCurrency && currencyEquals(currency, selectedCurrency))
|
||||
const otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency))
|
||||
const removeToken = useRemoveUserAddedToken()
|
||||
const addToken = useAddUserToken()
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
style={style}
|
||||
className={`token-item-${key}`}
|
||||
onClick={() => (isSelected ? null : onCurrencySelect(currency))}
|
||||
onClick={() => (isSelected ? null : onSelect())}
|
||||
disabled={isSelected}
|
||||
selected={otherSelected}
|
||||
>
|
||||
<RowFixed>
|
||||
<CurrencyLogo currency={currency} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<CurrencyLogo currency={currency} size={'24px'} />
|
||||
<Column>
|
||||
<Text fontWeight={500}>{currency.symbol}</Text>
|
||||
<Text title={currency.name} fontWeight={500}>
|
||||
{currency.symbol}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
{customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
@ -76,14 +122,14 @@ export default function CurrencyList({
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (currency instanceof Token) removeToken(chainId, currency.address)
|
||||
if (chainId && currency instanceof Token) removeToken(chainId, currency.address)
|
||||
}}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
{!isDefault && !customAdded ? (
|
||||
{!isOnSelectedList && !customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Found by address
|
||||
<LinkStyledButton
|
||||
@ -98,58 +144,65 @@ export default function CurrencyList({
|
||||
) : null}
|
||||
</FadedSpan>
|
||||
</Column>
|
||||
<TokenTags currency={currency} />
|
||||
<RowFixed style={{ justifySelf: 'flex-end' }}>
|
||||
{balance ? <Balance balance={balance} /> : account ? <Loader /> : null}
|
||||
</RowFixed>
|
||||
<AutoColumn>
|
||||
{balance ? (
|
||||
<Text>
|
||||
{zeroBalance && showSendWithSwap ? (
|
||||
<ButtonSecondary padding={'4px 8px'}>
|
||||
<Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
Send With Swap
|
||||
</Text>
|
||||
</ButtonSecondary>
|
||||
) : balance ? (
|
||||
balance.toSignificant(6)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
) : account ? (
|
||||
<Loader />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</AutoColumn>
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
}, [
|
||||
ETHBalance,
|
||||
account,
|
||||
addToken,
|
||||
allBalances,
|
||||
allTokens,
|
||||
chainId,
|
||||
}
|
||||
|
||||
export default function CurrencyList({
|
||||
height,
|
||||
currencies,
|
||||
defaultTokens,
|
||||
selectedCurrency,
|
||||
onCurrencySelect,
|
||||
otherCurrency,
|
||||
removeToken,
|
||||
selectedCurrency,
|
||||
showSendWithSwap,
|
||||
theme.primary1
|
||||
])
|
||||
fixedListRef,
|
||||
showETH
|
||||
}: {
|
||||
height: number
|
||||
currencies: Currency[]
|
||||
selectedCurrency: Currency | undefined
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherCurrency: Currency | undefined
|
||||
fixedListRef?: MutableRefObject<FixedSizeList | undefined>
|
||||
showETH: boolean
|
||||
}) {
|
||||
const itemData = useMemo(() => (showETH ? [Currency.ETHER, ...currencies] : currencies), [currencies, showETH])
|
||||
|
||||
const Row = useCallback(
|
||||
({ data, index, style }) => {
|
||||
const currency: Currency = data[index]
|
||||
const isSelected = Boolean(selectedCurrency && currencyEquals(selectedCurrency, currency))
|
||||
const otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency))
|
||||
const handleSelect = () => onCurrencySelect(currency)
|
||||
return (
|
||||
<CurrencyRow
|
||||
style={style}
|
||||
currency={currency}
|
||||
isSelected={isSelected}
|
||||
onSelect={handleSelect}
|
||||
otherSelected={otherSelected}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[onCurrencySelect, otherCurrency, selectedCurrency]
|
||||
)
|
||||
|
||||
const itemKey = useCallback((index: number, data: any) => currencyKey(data[index]), [])
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
height={500}
|
||||
itemCount={currencies.length + 1}
|
||||
itemData={itemData}
|
||||
itemCount={itemData.length}
|
||||
itemSize={56}
|
||||
style={{ flex: '1' }}
|
||||
itemKey={index => currencyKey(currencies[index])}
|
||||
itemKey={itemKey}
|
||||
>
|
||||
{CurrencyRow}
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)
|
||||
}
|
||||
|
210
src/components/SearchModal/CurrencySearch.tsx
Normal file
210
src/components/SearchModal/CurrencySearch.tsx
Normal file
@ -0,0 +1,210 @@
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { KeyboardEvent, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import { useSelectedListInfo } from '../../state/lists/hooks'
|
||||
import { CloseIcon, LinkStyledButton, TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import Card from '../Card'
|
||||
import Column from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import CommonBases from './CommonBases'
|
||||
import CurrencyList from './CurrencyList'
|
||||
import { filterTokens } from './filtering'
|
||||
import SortButton from './SortButton'
|
||||
import { useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput, Separator } from './styleds'
|
||||
import AutoSizer from 'react-virtualized-auto-sizer'
|
||||
|
||||
interface CurrencySearchProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
selectedCurrency?: Currency
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherSelectedCurrency?: Currency
|
||||
showCommonBases?: boolean
|
||||
onChangeList: () => void
|
||||
}
|
||||
|
||||
export function CurrencySearch({
|
||||
selectedCurrency,
|
||||
onCurrencySelect,
|
||||
otherSelectedCurrency,
|
||||
showCommonBases,
|
||||
onDismiss,
|
||||
isOpen,
|
||||
onChangeList
|
||||
}: CurrencySearchProps) {
|
||||
const { t } = useTranslation()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const fixedList = useRef<FixedSizeList>()
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
// if they input an address, use it
|
||||
const isAddressSearch = isAddress(searchQuery)
|
||||
const searchToken = useToken(searchQuery)
|
||||
|
||||
useEffect(() => {
|
||||
if (isAddressSearch) {
|
||||
ReactGA.event({
|
||||
category: 'Currency Select',
|
||||
action: 'Search by address',
|
||||
label: isAddressSearch
|
||||
})
|
||||
}
|
||||
}, [isAddressSearch])
|
||||
|
||||
const showETH: boolean = useMemo(() => {
|
||||
const s = searchQuery.toLowerCase().trim()
|
||||
return s === '' || s === 'e' || s === 'et' || s === 'eth'
|
||||
}, [searchQuery])
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (isAddressSearch) return searchToken ? [searchToken] : []
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [isAddressSearch, searchToken, allTokens, searchQuery])
|
||||
|
||||
const filteredSortedTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
const sorted = filteredTokens.sort(tokenComparator)
|
||||
const symbolMatch = searchQuery
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
if (symbolMatch.length > 1) return sorted
|
||||
|
||||
return [
|
||||
...(searchToken ? [searchToken] : []),
|
||||
// sort any exact symbol matches first
|
||||
...sorted.filter(token => token.symbol?.toLowerCase() === symbolMatch[0]),
|
||||
...sorted.filter(token => token.symbol?.toLowerCase() !== symbolMatch[0])
|
||||
]
|
||||
}, [filteredTokens, searchQuery, searchToken, tokenComparator])
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
onCurrencySelect(currency)
|
||||
onDismiss()
|
||||
},
|
||||
[onDismiss, onCurrencySelect]
|
||||
)
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
const handleInput = useCallback(event => {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
fixedList.current?.scrollTo(0)
|
||||
}, [])
|
||||
|
||||
const handleEnter = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
const s = searchQuery.toLowerCase().trim()
|
||||
if (s === 'eth') {
|
||||
handleCurrencySelect(ETHER)
|
||||
} else if (filteredSortedTokens.length > 0) {
|
||||
if (
|
||||
filteredSortedTokens[0].symbol?.toLowerCase() === searchQuery.trim().toLowerCase() ||
|
||||
filteredSortedTokens.length === 1
|
||||
) {
|
||||
handleCurrencySelect(filteredSortedTokens[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[filteredSortedTokens, handleCurrencySelect, searchQuery]
|
||||
)
|
||||
|
||||
const selectedListInfo = useSelectedListInfo()
|
||||
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn gap="14px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a token
|
||||
<QuestionHelper text="Find a token by searching for its name or symbol or by pasting its address below." />
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnter}
|
||||
/>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={selectedCurrency} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Token Name
|
||||
</Text>
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div style={{ flex: '1' }}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<CurrencyList
|
||||
height={height}
|
||||
showETH={showETH}
|
||||
currencies={filteredSortedTokens}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
otherCurrency={otherSelectedCurrency}
|
||||
selectedCurrency={selectedCurrency}
|
||||
fixedListRef={fixedList}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Card>
|
||||
<RowBetween>
|
||||
{selectedListInfo.current ? (
|
||||
<Row>
|
||||
{selectedListInfo.current.logoURI ? (
|
||||
<ListLogo
|
||||
style={{ marginRight: 12 }}
|
||||
logoURI={selectedListInfo.current.logoURI}
|
||||
alt={`${selectedListInfo.current.name} list logo`}
|
||||
/>
|
||||
) : null}
|
||||
<TYPE.main>{selectedListInfo.current.name}</TYPE.main>
|
||||
</Row>
|
||||
) : null}
|
||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={onChangeList}>
|
||||
{selectedListInfo.current ? 'Change' : 'Select a list'}
|
||||
</LinkStyledButton>
|
||||
</RowBetween>
|
||||
</Card>
|
||||
</Column>
|
||||
)
|
||||
}
|
@ -1,34 +1,18 @@
|
||||
import { Currency, Token } from '@uniswap/sdk'
|
||||
import React, { KeyboardEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import { useAllTokenBalances, useTokenBalance } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, LinkStyledButton } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import { Currency } from '@uniswap/sdk'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import useLast from '../../hooks/useLast'
|
||||
import { useSelectedListUrl } from '../../state/lists/hooks'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterTokens } from './filtering'
|
||||
import { useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import CurrencyList from './CurrencyList'
|
||||
import SortButton from './SortButton'
|
||||
import { CurrencySearch } from './CurrencySearch'
|
||||
import ListIntroduction from './ListIntroduction'
|
||||
import { ListSelect } from './ListSelect'
|
||||
|
||||
interface CurrencySearchModalProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
hiddenCurrency?: Currency
|
||||
showSendWithSwap?: boolean
|
||||
onCurrencySelect?: (currency: Currency) => void
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
selectedCurrency?: Currency
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherSelectedCurrency?: Currency
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
@ -37,53 +21,18 @@ export default function CurrencySearchModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onCurrencySelect,
|
||||
hiddenCurrency,
|
||||
showSendWithSwap,
|
||||
selectedCurrency,
|
||||
otherSelectedCurrency,
|
||||
showCommonBases = false
|
||||
}: CurrencySearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
const [listView, setListView] = useState<boolean>(false)
|
||||
const lastOpen = useLast(isOpen)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||
const searchToken = useToken(searchQuery)
|
||||
const searchTokenBalance = useTokenBalance(account, searchToken)
|
||||
const allTokenBalances_ = useAllTokenBalances()
|
||||
const allTokenBalances = searchToken
|
||||
? {
|
||||
[searchToken.address]: searchTokenBalance
|
||||
useEffect(() => {
|
||||
if (isOpen && !lastOpen) {
|
||||
setListView(false)
|
||||
}
|
||||
: allTokenBalances_ ?? {}
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [searchToken, allTokens, searchQuery])
|
||||
|
||||
const filteredSortedTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
const sorted = filteredTokens.sort(tokenComparator)
|
||||
const symbolMatch = searchQuery
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
if (symbolMatch.length > 1) return sorted
|
||||
|
||||
return [
|
||||
...(searchToken ? [searchToken] : []),
|
||||
// sort any exact symbol matches first
|
||||
...sorted.filter(token => token.symbol.toLowerCase() === symbolMatch[0]),
|
||||
...sorted.filter(token => token.symbol.toLowerCase() !== symbolMatch[0])
|
||||
]
|
||||
}, [filteredTokens, searchQuery, searchToken, tokenComparator])
|
||||
}, [isOpen, lastOpen])
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
@ -93,114 +42,41 @@ export default function CurrencySearchModal({
|
||||
[onDismiss, onCurrencySelect]
|
||||
)
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
const handleInput = useCallback(event => {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
setTooltipOpen(false)
|
||||
const handleClickChangeList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Change Lists'
|
||||
})
|
||||
setListView(true)
|
||||
}, [])
|
||||
const handleClickBack = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Back'
|
||||
})
|
||||
setListView(false)
|
||||
}, [])
|
||||
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
setTooltipOpen(false)
|
||||
},
|
||||
tooltipOpen ? 4000 : null,
|
||||
false
|
||||
)
|
||||
|
||||
const handleEnter = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && filteredSortedTokens.length > 0) {
|
||||
if (
|
||||
filteredSortedTokens[0].symbol.toLowerCase() === searchQuery.trim().toLowerCase() ||
|
||||
filteredSortedTokens.length === 1
|
||||
) {
|
||||
handleCurrencySelect(filteredSortedTokens[0])
|
||||
}
|
||||
}
|
||||
},
|
||||
[filteredSortedTokens, handleCurrencySelect, searchQuery]
|
||||
)
|
||||
const selectedListUrl = useSelectedListUrl()
|
||||
const noListSelected = !selectedListUrl
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} minHeight={noListSelected || listView ? 40 : 70}>
|
||||
{noListSelected ? (
|
||||
<ListIntroduction />
|
||||
) : listView ? (
|
||||
<ListSelect onDismiss={onDismiss} onBack={handleClickBack} />
|
||||
) : (
|
||||
<CurrencySearch
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
maxHeight={70}
|
||||
initialFocusRef={isMobile ? undefined : inputRef}
|
||||
minHeight={70}
|
||||
>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="14px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a token
|
||||
<QuestionHelper
|
||||
disabled={tooltipOpen}
|
||||
text="Find a token by searching for its name or symbol or by pasting its address below."
|
||||
/>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<Tooltip
|
||||
text="Import any token into your list by pasting the token address into the search field."
|
||||
show={tooltipOpen}
|
||||
placement="bottom"
|
||||
>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={handleInput}
|
||||
onFocus={closeTooltip}
|
||||
onBlur={closeTooltip}
|
||||
onKeyDown={handleEnter}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={hiddenCurrency} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Token Name
|
||||
</Text>
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<CurrencyList
|
||||
currencies={filteredSortedTokens}
|
||||
allBalances={allTokenBalances}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
otherCurrency={otherSelectedCurrency}
|
||||
selectedCurrency={hiddenCurrency}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
onChangeList={handleClickChangeList}
|
||||
selectedCurrency={selectedCurrency}
|
||||
otherSelectedCurrency={otherSelectedCurrency}
|
||||
showCommonBases={showCommonBases}
|
||||
/>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
|
||||
Having trouble finding a token?
|
||||
</LinkStyledButton>
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
80
src/components/SearchModal/ListIntroduction.tsx
Normal file
80
src/components/SearchModal/ListIntroduction.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React, { memo, useCallback, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Text } from 'rebass'
|
||||
import { AppDispatch, AppState } from '../../state'
|
||||
import { addList, selectList } from '../../state/lists/actions'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { OutlineCard, GreyCard } from '../Card'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import Row from '../Row'
|
||||
import { PaddedColumn } from './styleds'
|
||||
|
||||
const ListCard = memo(function ListCard({ id, listUrl }: { id: string; listUrl: string }) {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const list = listsByUrl[listUrl]?.current
|
||||
|
||||
useEffect(() => {
|
||||
if (!listsByUrl[listUrl]) dispatch(addList(listUrl))
|
||||
}, [dispatch, listUrl, listsByUrl])
|
||||
|
||||
const handleSelect = useCallback(() => {
|
||||
dispatch(selectList(listUrl))
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
if (!list) return null
|
||||
|
||||
return (
|
||||
<OutlineCard style={{ padding: '0.5rem .75rem' }} id={id}>
|
||||
<Row align="center">
|
||||
{list.logoURI ? (
|
||||
<ListLogo style={{ marginRight: '0.5rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
|
||||
) : null}
|
||||
<Text fontWeight={500} style={{ flex: '1' }}>
|
||||
{list.name}
|
||||
</Text>
|
||||
<ButtonPrimary
|
||||
className="select-button"
|
||||
style={{ width: '6rem', padding: '0.5rem .35rem', borderRadius: '12px' }}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
Select
|
||||
</ButtonPrimary>
|
||||
</Row>
|
||||
</OutlineCard>
|
||||
)
|
||||
})
|
||||
|
||||
export default function ListIntroduction() {
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn>
|
||||
<AutoColumn gap="14px">
|
||||
<Text fontWeight={600} fontSize={20}>
|
||||
Select a list
|
||||
</Text>
|
||||
<Text style={{ marginBottom: '8px' }}>
|
||||
Get started by selecting a token list below. You can switch between token lists and add your own custom
|
||||
lists via IPFS, HTTPS and ENS.
|
||||
</Text>
|
||||
<ListCard id="select-kleros-list" listUrl={'t2crtokens.eth'} />
|
||||
<ListCard
|
||||
id="select-1inch-list"
|
||||
listUrl={'https://www.coingecko.com/tokens_list/uniswap/defi_100/v_0_0_0.json'}
|
||||
/>
|
||||
<ListCard id="select-default-uniswap-list" listUrl={'tokens.uniswap.eth'} />
|
||||
<GreyCard style={{ marginBottom: '8px', padding: '1rem' }}>
|
||||
<Text fontWeight={400} fontSize={14} style={{ textAlign: 'center' }}>
|
||||
Token lists are an{' '}
|
||||
<ExternalLink href="https://github.com/uniswap/token-lists">open specification</ExternalLink>. Check out{' '}
|
||||
<ExternalLink href="https://tokenlists.org">tokenlists.org</ExternalLink> to find more lists.
|
||||
</Text>
|
||||
</GreyCard>
|
||||
</AutoColumn>
|
||||
</PaddedColumn>
|
||||
</Column>
|
||||
)
|
||||
}
|
372
src/components/SearchModal/ListSelect.tsx
Normal file
372
src/components/SearchModal/ListSelect.tsx
Normal file
@ -0,0 +1,372 @@
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { usePopper } from 'react-popper'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
import { AppDispatch, AppState } from '../../state'
|
||||
import { acceptListUpdate, removeList, selectList } from '../../state/lists/actions'
|
||||
import { useSelectedListUrl } from '../../state/lists/hooks'
|
||||
import { CloseIcon, ExternalLink, LinkStyledButton, TYPE } from '../../theme'
|
||||
import listVersionLabel from '../../utils/listVersionLabel'
|
||||
import { parseENSAddress } from '../../utils/parseENSAddress'
|
||||
import uriToHttp from '../../utils/uriToHttp'
|
||||
import { ButtonOutlined, ButtonPrimary, ButtonSecondary } from '../Button'
|
||||
|
||||
import Column from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
|
||||
|
||||
const UnpaddedLinkStyledButton = styled(LinkStyledButton)`
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 100;
|
||||
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
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);
|
||||
color: ${({ theme }) => theme.text2};
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 8px;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
`
|
||||
|
||||
const StyledListUrlText = styled.div`
|
||||
max-width: 160px;
|
||||
opacity: 0.6;
|
||||
margin-right: 0.5rem;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
function ListOrigin({ listUrl }: { listUrl: string }) {
|
||||
const ensName = useMemo(() => parseENSAddress(listUrl)?.ensName, [listUrl])
|
||||
const host = useMemo(() => {
|
||||
if (ensName) return undefined
|
||||
try {
|
||||
const url = new URL(listUrl)
|
||||
return url.host
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}, [listUrl, ensName])
|
||||
return <>{ensName ?? host}</>
|
||||
}
|
||||
|
||||
const ListRow = memo(function ListRow({ listUrl, onBack }: { listUrl: string; onBack: () => void }) {
|
||||
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const selectedListUrl = useSelectedListUrl()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]
|
||||
|
||||
const isSelected = listUrl === selectedListUrl
|
||||
|
||||
const [open, toggle] = useToggle(false)
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>()
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>()
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'auto',
|
||||
strategy: 'fixed',
|
||||
modifiers: [{ name: 'offset', options: { offset: [8, 8] } }]
|
||||
})
|
||||
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
const selectThisList = useCallback(() => {
|
||||
if (isSelected) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Select List',
|
||||
label: listUrl
|
||||
})
|
||||
|
||||
dispatch(selectList(listUrl))
|
||||
onBack()
|
||||
}, [dispatch, isSelected, listUrl, onBack])
|
||||
|
||||
const handleAcceptListUpdate = useCallback(() => {
|
||||
if (!pending) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Update List from List Select',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
}, [dispatch, listUrl, pending])
|
||||
|
||||
const handleRemoveList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Start Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
if (window.prompt(`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Confirm Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(removeList(listUrl))
|
||||
}
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
if (!list) return null
|
||||
|
||||
return (
|
||||
<Row key={listUrl} align="center" padding="16px">
|
||||
{list.logoURI ? (
|
||||
<ListLogo style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
|
||||
) : (
|
||||
<div style={{ width: '24px', height: '24px', marginRight: '1rem' }} />
|
||||
)}
|
||||
<Column style={{ flex: '1' }}>
|
||||
<Row>
|
||||
<Text
|
||||
fontWeight={isSelected ? 500 : 400}
|
||||
fontSize={16}
|
||||
style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
title={listUrl}
|
||||
>
|
||||
{list.name}
|
||||
</Text>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
marginTop: '4px'
|
||||
}}
|
||||
>
|
||||
<StyledListUrlText title={listUrl}>
|
||||
<ListOrigin listUrl={listUrl} />
|
||||
</StyledListUrlText>
|
||||
</Row>
|
||||
</Column>
|
||||
<StyledMenu ref={node as any}>
|
||||
<ButtonOutlined
|
||||
style={{
|
||||
width: '2rem',
|
||||
padding: '.8rem .35rem',
|
||||
borderRadius: '12px',
|
||||
fontSize: '14px',
|
||||
marginRight: '0.5rem'
|
||||
}}
|
||||
onClick={toggle}
|
||||
ref={setReferenceElement}
|
||||
>
|
||||
<DropDown />
|
||||
</ButtonOutlined>
|
||||
|
||||
{open && (
|
||||
<PopoverContainer show={true} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
|
||||
<div>{list && listVersionLabel(list.version)}</div>
|
||||
<SeparatorDark />
|
||||
<ExternalLink href={`https://tokenlists.org/token-list?url=${listUrl}`}>View list</ExternalLink>
|
||||
<UnpaddedLinkStyledButton onClick={handleRemoveList} disabled={Object.keys(listsByUrl).length === 1}>
|
||||
Remove list
|
||||
</UnpaddedLinkStyledButton>
|
||||
{pending && (
|
||||
<UnpaddedLinkStyledButton onClick={handleAcceptListUpdate}>Update list</UnpaddedLinkStyledButton>
|
||||
)}
|
||||
</PopoverContainer>
|
||||
)}
|
||||
</StyledMenu>
|
||||
{isSelected ? (
|
||||
<ButtonPrimary
|
||||
disabled={true}
|
||||
className="select-button"
|
||||
style={{ width: '5rem', minWidth: '5rem', padding: '0.5rem .35rem', borderRadius: '12px', fontSize: '14px' }}
|
||||
>
|
||||
Selected
|
||||
</ButtonPrimary>
|
||||
) : (
|
||||
<>
|
||||
<ButtonPrimary
|
||||
className="select-button"
|
||||
style={{
|
||||
width: '5rem',
|
||||
minWidth: '4.5rem',
|
||||
padding: '0.5rem .35rem',
|
||||
borderRadius: '12px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
onClick={selectThisList}
|
||||
>
|
||||
Select
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
|
||||
const AddListButton = styled(ButtonSecondary)`
|
||||
/* height: 1.8rem; */
|
||||
max-width: 4rem;
|
||||
margin-left: 1rem;
|
||||
border-radius: 12px;
|
||||
padding: 10px 18px;
|
||||
`
|
||||
|
||||
const ListContainer = styled.div`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
export function ListSelect({ onDismiss, onBack }: { onDismiss: () => void; onBack: () => void }) {
|
||||
const [listUrlInput, setListUrlInput] = useState<string>('')
|
||||
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const adding = Boolean(lists[listUrlInput]?.loadingRequestId)
|
||||
const [addError, setAddError] = useState<string | null>(null)
|
||||
|
||||
const handleInput = useCallback(e => {
|
||||
setListUrlInput(e.target.value)
|
||||
setAddError(null)
|
||||
}, [])
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
const handleAddList = useCallback(() => {
|
||||
if (adding) return
|
||||
setAddError(null)
|
||||
fetchList(listUrlInput)
|
||||
.then(() => {
|
||||
setListUrlInput('')
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List',
|
||||
label: listUrlInput
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List Failed',
|
||||
label: listUrlInput
|
||||
})
|
||||
setAddError(error.message)
|
||||
dispatch(removeList(listUrlInput))
|
||||
})
|
||||
}, [adding, dispatch, fetchList, listUrlInput])
|
||||
|
||||
const validUrl: boolean = useMemo(() => {
|
||||
return uriToHttp(listUrlInput).length > 0 || Boolean(parseENSAddress(listUrlInput))
|
||||
}, [listUrlInput])
|
||||
|
||||
const handleEnterKey = useCallback(
|
||||
e => {
|
||||
if (validUrl && e.key === 'Enter') {
|
||||
handleAddList()
|
||||
}
|
||||
},
|
||||
[handleAddList, validUrl]
|
||||
)
|
||||
|
||||
const sortedLists = useMemo(() => {
|
||||
const listUrls = Object.keys(lists)
|
||||
return listUrls
|
||||
.filter(listUrl => {
|
||||
return Boolean(lists[listUrl].current)
|
||||
})
|
||||
.sort((u1, u2) => {
|
||||
const { current: l1 } = lists[u1]
|
||||
const { current: l2 } = lists[u2]
|
||||
if (l1 && l2) {
|
||||
return l1.name.toLowerCase() < l2.name.toLowerCase()
|
||||
? -1
|
||||
: l1.name.toLowerCase() === l2.name.toLowerCase()
|
||||
? 0
|
||||
: 1
|
||||
}
|
||||
if (l1) return -1
|
||||
if (l2) return 1
|
||||
return 0
|
||||
})
|
||||
}, [lists])
|
||||
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn>
|
||||
<RowBetween>
|
||||
<div>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} />
|
||||
</div>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Manage Lists
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
|
||||
<PaddedColumn gap="14px">
|
||||
<Text fontWeight={600}>
|
||||
Add a list{' '}
|
||||
<QuestionHelper text="Token lists are an open specification for lists of ERC20 tokens. You can use any token list by entering its URL below. Beware that third party token lists can contain fake or malicious ERC20 tokens." />
|
||||
</Text>
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="list-add-input"
|
||||
placeholder="https:// or ipfs:// or ENS name"
|
||||
value={listUrlInput}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnterKey}
|
||||
style={{ height: '2.75rem', borderRadius: 12, padding: '12px' }}
|
||||
/>
|
||||
<AddListButton onClick={handleAddList} disabled={!validUrl}>
|
||||
Add
|
||||
</AddListButton>
|
||||
</Row>
|
||||
{addError ? (
|
||||
<TYPE.error title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
|
||||
{addError}
|
||||
</TYPE.error>
|
||||
) : null}
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
|
||||
<ListContainer>
|
||||
{sortedLists.map(listUrl => (
|
||||
<ListRow key={listUrl} listUrl={listUrl} onBack={onBack} />
|
||||
))}
|
||||
</ListContainer>
|
||||
<Separator />
|
||||
|
||||
<ExternalLink style={{ margin: '16px', textAlign: 'center' }} href="https://tokenlists.org">
|
||||
Browse lists
|
||||
</ExternalLink>
|
||||
</Column>
|
||||
)
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import { Token, TokenAmount } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokenBalances } from '../../state/wallet/hooks'
|
||||
|
||||
// compare two token amounts with highest one coming first
|
||||
@ -15,20 +14,13 @@ function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
return 0
|
||||
}
|
||||
|
||||
function getTokenComparator(
|
||||
weth: Token | undefined,
|
||||
balances: { [tokenAddress: string]: TokenAmount }
|
||||
): (tokenA: Token, tokenB: Token) => number {
|
||||
function getTokenComparator(balances: {
|
||||
[tokenAddress: string]: TokenAmount | undefined
|
||||
}): (tokenA: Token, tokenB: Token) => number {
|
||||
return function sortTokens(tokenA: Token, tokenB: Token): number {
|
||||
// -1 = a is first
|
||||
// 1 = b is first
|
||||
|
||||
// sort ETH first
|
||||
if (weth) {
|
||||
if (tokenA.equals(weth)) return -1
|
||||
if (tokenB.equals(weth)) return 1
|
||||
}
|
||||
|
||||
// sort by balances
|
||||
const balanceA = balances[tokenA.address]
|
||||
const balanceB = balances[tokenB.address]
|
||||
@ -36,16 +28,18 @@ function getTokenComparator(
|
||||
const balanceComp = balanceComparator(balanceA, balanceB)
|
||||
if (balanceComp !== 0) return balanceComp
|
||||
|
||||
if (tokenA.symbol && tokenB.symbol) {
|
||||
// sort by symbol
|
||||
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
|
||||
} else {
|
||||
return tokenA.symbol ? -1 : tokenB.symbol ? -1 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const weth = WETH[chainId]
|
||||
const balances = useAllTokenBalances()
|
||||
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
|
||||
const comparator = useMemo(() => getTokenComparator(balances ?? {}), [balances])
|
||||
return useMemo(() => {
|
||||
if (inverted) {
|
||||
return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1
|
||||
|
@ -17,12 +17,26 @@ export const FadedSpan = styled(RowFixed)`
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
export const GreySpan = styled.span`
|
||||
color: ${({ theme }) => theme.text3};
|
||||
font-weight: 400;
|
||||
export const PaddedColumn = styled(AutoColumn)`
|
||||
padding: 20px;
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
export const Input = styled.input`
|
||||
export const MenuItem = styled(RowBetween)`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(auto, 1fr) auto minmax(0, 72px);
|
||||
grid-gap: 16px;
|
||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||
:hover {
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
|
||||
}
|
||||
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
|
||||
`
|
||||
|
||||
export const SearchInput = styled.input`
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
@ -43,28 +57,20 @@ export const Input = styled.input`
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.text3};
|
||||
}
|
||||
`
|
||||
|
||||
export const PaddedColumn = styled(AutoColumn)`
|
||||
padding: 20px;
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
export const MenuItem = styled(RowBetween)`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||
:hover {
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
|
||||
}
|
||||
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
|
||||
`
|
||||
|
||||
export const SearchInput = styled(Input)`
|
||||
transition: border 100ms;
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
export const Separator = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
`
|
||||
|
||||
export const SeparatorDark = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
4
src/components/SearchModal/tsconfig.json
Normal file
4
src/components/SearchModal/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
import React, { useRef, useEffect, useContext, useState } from 'react'
|
||||
import React, { useRef, useContext, useState } from 'react'
|
||||
import { Settings, X } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import {
|
||||
useUserSlippageTolerance,
|
||||
useExpertModeManager,
|
||||
useUserDeadline,
|
||||
useDarkModeManager
|
||||
} from '../../state/user/hooks'
|
||||
import SlippageTabs from '../SlippageTabs'
|
||||
import TransactionSettings from '../TransactionSettings'
|
||||
import { RowFixed, RowBetween } from '../Row'
|
||||
import { TYPE } from '../../theme'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
@ -138,24 +138,7 @@ export default function SettingsTab() {
|
||||
// show confirmation view before turning on
|
||||
const [showConfirmation, setShowConfirmation] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = e => {
|
||||
if (node.current?.contains(e.target) ?? false) {
|
||||
return
|
||||
}
|
||||
toggle()
|
||||
}
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [open, toggle])
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
@ -212,7 +195,7 @@ export default function SettingsTab() {
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
Transaction Settings
|
||||
</Text>
|
||||
<SlippageTabs
|
||||
<TransactionSettings
|
||||
rawSlippage={userSlippageTolerance}
|
||||
setRawSlippage={setUserslippageTolerance}
|
||||
deadline={deadline}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledRangeInput = styled.input<{ value: number }>`
|
||||
const StyledRangeInput = styled.input<{ size: number }>`
|
||||
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
|
||||
width: 100%; /* Specific width is required for Firefox. */
|
||||
background: transparent; /* Otherwise white in Chrome */
|
||||
@ -17,8 +17,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: ${({ size }) => size}px;
|
||||
width: ${({ size }) => size}px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
@ -33,8 +33,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: ${({ size }) => size}px;
|
||||
width: ${({ size }) => size}px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
@ -48,8 +48,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
}
|
||||
|
||||
&::-ms-thumb {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: ${({ size }) => size}px;
|
||||
width: ${({ size }) => size}px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
color: ${({ theme }) => theme.bg1};
|
||||
@ -62,24 +62,12 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme }) => theme.bg5},
|
||||
${({ theme }) => theme.bg5} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3}
|
||||
);
|
||||
background: linear-gradient(90deg, ${({ theme }) => theme.bg5}, ${({ theme }) => theme.bg3});
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme }) => theme.bg5},
|
||||
${({ theme }) => theme.bg5} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3}
|
||||
);
|
||||
background: linear-gradient(90deg, ${({ theme }) => theme.bg5}, ${({ theme }) => theme.bg3});
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
@ -102,26 +90,31 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
interface InputSliderProps {
|
||||
value: number
|
||||
onChange: (value: number) => void
|
||||
step?: number
|
||||
min?: number
|
||||
max?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
export default function InputSlider({ value, onChange }: InputSliderProps) {
|
||||
export default function Slider({ value, onChange, min = 0, step = 1, max = 100, size = 28 }: InputSliderProps) {
|
||||
const changeCallback = useCallback(
|
||||
e => {
|
||||
onChange(e.target.value)
|
||||
onChange(parseInt(e.target.value))
|
||||
},
|
||||
[onChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<StyledRangeInput
|
||||
size={size}
|
||||
type="range"
|
||||
value={value}
|
||||
style={{ width: '90%', marginLeft: 15, marginRight: 15, padding: '15px 0' }}
|
||||
onChange={changeCallback}
|
||||
aria-labelledby="input-slider"
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
aria-labelledby="input slider"
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1,139 +0,0 @@
|
||||
import { Currency, Token } from '@uniswap/sdk'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useDefaultTokenList } from '../../state/lists/hooks'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink, isDefaultToken } from '../../utils'
|
||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { ButtonError } from '../Button'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
|
||||
const Wrapper = styled.div<{ error: boolean }>`
|
||||
background: ${({ theme }) => transparentize(0.6, theme.white)};
|
||||
padding: 0.75rem;
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
const WarningContainer = styled.div`
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background: rgba(242, 150, 2, 0.05);
|
||||
border: 1px solid #f3841e;
|
||||
box-sizing: border-box;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 2rem;
|
||||
`
|
||||
|
||||
const StyledWarningIcon = styled(AlertTriangle)`
|
||||
stroke: ${({ theme }) => theme.red2};
|
||||
`
|
||||
|
||||
interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'> {
|
||||
token?: Token
|
||||
}
|
||||
|
||||
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const defaultTokens = useDefaultTokenList()
|
||||
const isDefault = isDefaultToken(defaultTokens, token)
|
||||
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
const duplicateNameOrSymbol = useMemo(() => {
|
||||
if (isDefault || !token || !chainId) return false
|
||||
|
||||
return Object.keys(allTokens).some(tokenAddress => {
|
||||
const userToken = allTokens[tokenAddress]
|
||||
if (userToken.equals(token)) {
|
||||
return false
|
||||
}
|
||||
return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName
|
||||
})
|
||||
}, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
|
||||
if (isDefault || !token) return null
|
||||
|
||||
return (
|
||||
<Wrapper error={duplicateNameOrSymbol} {...rest}>
|
||||
<AutoRow gap="6px">
|
||||
<AutoColumn gap="24px">
|
||||
<CurrencyLogo currency={token} size={'16px'} />
|
||||
<div> </div>
|
||||
</AutoColumn>
|
||||
<AutoColumn gap="10px" justify="flex-start">
|
||||
<TYPE.main>
|
||||
{token && token.name && token.symbol && token.name !== token.symbol
|
||||
? `${token.name} (${token.symbol})`
|
||||
: token.name || token.symbol}
|
||||
</TYPE.main>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
|
||||
<TYPE.blue> (View on Etherscan)</TYPE.blue>
|
||||
</ExternalLink>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export function TokenWarningCards({ currencies }: { currencies: { [field in Field]?: Currency } }) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const [dismissedToken0, dismissToken0] = useTokenWarningDismissal(chainId, currencies[Field.INPUT])
|
||||
const [dismissedToken1, dismissToken1] = useTokenWarningDismissal(chainId, currencies[Field.OUTPUT])
|
||||
|
||||
return (
|
||||
<WarningContainer className="token-warning-container">
|
||||
<AutoColumn gap="lg">
|
||||
<AutoRow gap="6px">
|
||||
<StyledWarningIcon />
|
||||
<TYPE.main color={'red2'}>Token imported</TYPE.main>
|
||||
</AutoRow>
|
||||
<TYPE.body color={'red2'}>
|
||||
Anyone can create and name any ERC20 token on Ethereum, including creating fake versions of existing tokens
|
||||
and tokens that claim to represent projects that do not have a token.
|
||||
</TYPE.body>
|
||||
<TYPE.body color={'red2'}>
|
||||
Similar to Etherscan, this site can load arbitrary tokens via token addresses. Please do your own research
|
||||
before interacting with any ERC20 token.
|
||||
</TYPE.body>
|
||||
{Object.keys(currencies).map(field => {
|
||||
const dismissed = field === Field.INPUT ? dismissedToken0 : dismissedToken1
|
||||
return currencies[field] instanceof Token && !dismissed ? (
|
||||
<TokenWarningCard key={field} token={currencies[field]} />
|
||||
) : null
|
||||
})}
|
||||
<RowBetween>
|
||||
<div />
|
||||
<ButtonError
|
||||
error={true}
|
||||
width={'140px'}
|
||||
padding="0.5rem 1rem"
|
||||
style={{
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
onClick={() => {
|
||||
dismissToken0 && dismissToken0()
|
||||
dismissToken1 && dismissToken1()
|
||||
}}
|
||||
>
|
||||
<TYPE.body color="white" className="token-dismiss-button">
|
||||
I understand
|
||||
</TYPE.body>
|
||||
</ButtonError>
|
||||
<div />
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</WarningContainer>
|
||||
)
|
||||
}
|
151
src/components/TokenWarningModal/index.tsx
Normal file
151
src/components/TokenWarningModal/index.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink, shortenAddress } from '../../utils'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import Modal from '../Modal'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { ButtonError } from '../Button'
|
||||
|
||||
const Wrapper = styled.div<{ error: boolean }>`
|
||||
background: ${({ theme }) => transparentize(0.6, theme.bg3)};
|
||||
padding: 0.75rem;
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
const WarningContainer = styled.div`
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background: rgba(242, 150, 2, 0.05);
|
||||
border: 1px solid #f3841e;
|
||||
border-radius: 20px;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
const StyledWarningIcon = styled(AlertTriangle)`
|
||||
stroke: ${({ theme }) => theme.red2};
|
||||
`
|
||||
|
||||
interface TokenWarningCardProps {
|
||||
token?: Token
|
||||
}
|
||||
|
||||
function TokenWarningCard({ token }: TokenWarningCardProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
const duplicateNameOrSymbol = useMemo(() => {
|
||||
if (!token || !chainId) return false
|
||||
|
||||
return Object.keys(allTokens).some(tokenAddress => {
|
||||
const userToken = allTokens[tokenAddress]
|
||||
if (userToken.equals(token)) {
|
||||
return false
|
||||
}
|
||||
return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName
|
||||
})
|
||||
}, [token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
|
||||
if (!token) return null
|
||||
|
||||
return (
|
||||
<Wrapper error={duplicateNameOrSymbol}>
|
||||
<AutoRow gap="6px">
|
||||
<AutoColumn gap="24px">
|
||||
<CurrencyLogo currency={token} size={'16px'} />
|
||||
<div> </div>
|
||||
</AutoColumn>
|
||||
<AutoColumn gap="10px" justify="flex-start">
|
||||
<TYPE.main>
|
||||
{token && token.name && token.symbol && token.name !== token.symbol
|
||||
? `${token.name} (${token.symbol})`
|
||||
: token.name || token.symbol}{' '}
|
||||
</TYPE.main>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
|
||||
<TYPE.blue title={token.address}>{shortenAddress(token.address)} (View on Etherscan)</TYPE.blue>
|
||||
</ExternalLink>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TokenWarningModal({
|
||||
isOpen,
|
||||
tokens,
|
||||
onConfirm
|
||||
}: {
|
||||
isOpen: boolean
|
||||
tokens: Token[]
|
||||
onConfirm: () => void
|
||||
}) {
|
||||
const [understandChecked, setUnderstandChecked] = useState(false)
|
||||
const toggleUnderstand = useCallback(() => setUnderstandChecked(uc => !uc), [])
|
||||
|
||||
const handleDismiss = useCallback(() => null, [])
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={handleDismiss} maxHeight={90}>
|
||||
<WarningContainer className="token-warning-container">
|
||||
<AutoColumn gap="lg">
|
||||
<AutoRow gap="6px">
|
||||
<StyledWarningIcon />
|
||||
<TYPE.main color={'red2'}>Token imported</TYPE.main>
|
||||
</AutoRow>
|
||||
<TYPE.body color={'red2'}>
|
||||
Anyone can create an ERC20 token on Ethereum with <em>any</em> name, including creating fake versions of
|
||||
existing tokens and tokens that claim to represent projects that do not have a token.
|
||||
</TYPE.body>
|
||||
<TYPE.body color={'red2'}>
|
||||
This interface can load arbitrary tokens by token addresses. Please take extra caution and do your research
|
||||
when interacting with arbitrary ERC20 tokens.
|
||||
</TYPE.body>
|
||||
<TYPE.body color={'red2'}>
|
||||
If you purchase an arbitrary token, <strong>you may be unable to sell it back.</strong>
|
||||
</TYPE.body>
|
||||
{tokens.map(token => {
|
||||
return <TokenWarningCard key={token.address} token={token} />
|
||||
})}
|
||||
<RowBetween>
|
||||
<div>
|
||||
<label style={{ cursor: 'pointer', userSelect: 'none' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="understand-checkbox"
|
||||
checked={understandChecked}
|
||||
onChange={toggleUnderstand}
|
||||
/>{' '}
|
||||
I understand
|
||||
</label>
|
||||
</div>
|
||||
<ButtonError
|
||||
disabled={!understandChecked}
|
||||
error={true}
|
||||
width={'140px'}
|
||||
padding="0.5rem 1rem"
|
||||
className="token-dismiss-button"
|
||||
style={{
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
onClick={() => {
|
||||
onConfirm()
|
||||
}}
|
||||
>
|
||||
<TYPE.body color="white">Continue</TYPE.body>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</WarningContainer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
@ -30,7 +30,7 @@ export const SectionBreak = styled.div`
|
||||
`
|
||||
|
||||
export const BottomGrouping = styled.div`
|
||||
margin-top: 12px;
|
||||
margin-top: 1rem;
|
||||
`
|
||||
|
||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
|
||||
|
@ -152,6 +152,10 @@ export class NetworkConnector extends AbstractConnector {
|
||||
}, {})
|
||||
}
|
||||
|
||||
public get provider(): MiniRpcProvider {
|
||||
return this.providers[this.currentChainId]
|
||||
}
|
||||
|
||||
public async activate(): Promise<ConnectorUpdate> {
|
||||
return { provider: this.providers[this.currentChainId], chainId: this.currentChainId, account: null }
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { InjectedConnector } from '@web3-react/injected-connector'
|
||||
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
import { WalletLinkConnector } from '@web3-react/walletlink-connector'
|
||||
@ -10,14 +11,21 @@ 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
|
||||
|
||||
export const NETWORK_CHAIN_ID: number = parseInt(process.env.REACT_APP_CHAIN_ID ?? '1')
|
||||
|
||||
if (typeof NETWORK_URL === 'undefined') {
|
||||
throw new Error(`REACT_APP_NETWORK_URL must be a defined environment variable`)
|
||||
}
|
||||
|
||||
export const network = new NetworkConnector({
|
||||
urls: { [Number(process.env.REACT_APP_CHAIN_ID)]: NETWORK_URL }
|
||||
urls: { [NETWORK_CHAIN_ID]: NETWORK_URL }
|
||||
})
|
||||
|
||||
let networkLibrary: Web3Provider | undefined
|
||||
export function getNetworkLibrary(): Web3Provider {
|
||||
return (networkLibrary = networkLibrary ?? new Web3Provider(network.provider as any))
|
||||
}
|
||||
|
||||
export const injected = new InjectedConnector({
|
||||
supportedChainIds: [1, 3, 4, 5, 42]
|
||||
})
|
||||
|
816
src/constants/abis/ens-public-resolver.json
Normal file
816
src/constants/abis/ens-public-resolver.json
Normal file
@ -0,0 +1,816 @@
|
||||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "contract ENS",
|
||||
"name": "_ens",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "uint256",
|
||||
"name": "contentType",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "ABIChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "a",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "AddrChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "coinType",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes",
|
||||
"name": "newAddress",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "AddressChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "target",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bool",
|
||||
"name": "isAuthorised",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "AuthorisationChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes",
|
||||
"name": "hash",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "ContenthashChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes",
|
||||
"name": "name",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint16",
|
||||
"name": "resource",
|
||||
"type": "uint16"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes",
|
||||
"name": "record",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "DNSRecordChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes",
|
||||
"name": "name",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint16",
|
||||
"name": "resource",
|
||||
"type": "uint16"
|
||||
}
|
||||
],
|
||||
"name": "DNSRecordDeleted",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "DNSZoneCleared",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceID",
|
||||
"type": "bytes4"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "implementer",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "InterfaceChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "string",
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "NameChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes32",
|
||||
"name": "x",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes32",
|
||||
"name": "y",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "PubkeyChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "string",
|
||||
"name": "indexedKey",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "string",
|
||||
"name": "key",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "TextChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "contentTypes",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "ABI",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "addr",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address payable",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "authorisations",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "clearDNSZone",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "contenthash",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "name",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint16",
|
||||
"name": "resource",
|
||||
"type": "uint16"
|
||||
}
|
||||
],
|
||||
"name": "dnsRecord",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "name",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "hasDNSRecords",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceID",
|
||||
"type": "bytes4"
|
||||
}
|
||||
],
|
||||
"name": "interfaceImplementer",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "pubkey",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "x",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "y",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "contentType",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "data",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "setABI",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "coinType",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "a",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "setAddr",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "a",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "setAddr",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "target",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "isAuthorised",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "setAuthorisation",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "hash",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "setContenthash",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "data",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "setDNSRecords",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceID",
|
||||
"type": "bytes4"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "implementer",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "setInterface",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "setName",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "x",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "y",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "setPubkey",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "key",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "value",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "setText",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceID",
|
||||
"type": "bytes4"
|
||||
}
|
||||
],
|
||||
"name": "supportsInterface",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "pure",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "key",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "text",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
422
src/constants/abis/ens-registrar.json
Normal file
422
src/constants/abis/ens-registrar.json
Normal file
@ -0,0 +1,422 @@
|
||||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "contract ENS",
|
||||
"name": "_old",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bool",
|
||||
"name": "approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "ApprovalForAll",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "label",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "NewOwner",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "resolver",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "NewResolver",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint64",
|
||||
"name": "ttl",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"name": "NewTTL",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "isApprovedForAll",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "old",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "contract ENS",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "owner",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "recordExists",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "resolver",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "setApprovalForAll",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "setOwner",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "resolver",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint64",
|
||||
"name": "ttl",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"name": "setRecord",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "resolver",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "setResolver",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "label",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "setSubnodeOwner",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "label",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "resolver",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint64",
|
||||
"name": "ttl",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"name": "setSubnodeRecord",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint64",
|
||||
"name": "ttl",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"name": "setTTL",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "ttl",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint64",
|
||||
"name": "",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
@ -1,5 +1,5 @@
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
|
||||
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
|
||||
|
||||
@ -162,6 +162,3 @@ 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'
|
||||
|
14
src/constants/lists.ts
Normal file
14
src/constants/lists.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// the Uniswap Default token list lives here
|
||||
export const DEFAULT_TOKEN_LIST_URL = 'tokens.uniswap.eth'
|
||||
|
||||
export const DEFAULT_LIST_OF_LISTS: string[] = [
|
||||
DEFAULT_TOKEN_LIST_URL,
|
||||
't2crtokens.eth', // kleros
|
||||
'tokens.1inch.eth', // 1inch
|
||||
'synths.snx.eth',
|
||||
'tokenlist.dharma.eth',
|
||||
'defi.cmc.eth',
|
||||
'erc20.cmc.eth',
|
||||
'https://defiprime.com/defiprime.tokenlist.json',
|
||||
'https://umaproject.org/uma.tokenlist.json'
|
||||
]
|
@ -1,7 +1,7 @@
|
||||
import { parseBytes32String } from '@ethersproject/strings'
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useDefaultTokenList } from '../state/lists/hooks'
|
||||
import { useSelectedTokenList } from '../state/lists/hooks'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useUserAddedTokens } from '../state/user/hooks'
|
||||
import { isAddress } from '../utils'
|
||||
@ -12,7 +12,7 @@ import { useBytes32TokenContract, useTokenContract } from './useContract'
|
||||
export function useAllTokens(): { [address: string]: Token } {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
const allTokens = useDefaultTokenList()
|
||||
const allTokens = useSelectedTokenList()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!chainId) return {}
|
||||
|
@ -17,19 +17,29 @@ function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
|
||||
? [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
|
||||
: [undefined, undefined]
|
||||
|
||||
const basePairs: [Token, Token][] = useMemo(
|
||||
() =>
|
||||
flatMap(bases, (base): [Token, Token][] => bases.map(otherBase => [base, otherBase])).filter(
|
||||
([t0, t1]) => t0.address !== t1.address
|
||||
),
|
||||
[bases]
|
||||
)
|
||||
|
||||
const allPairCombinations: [Token, Token][] = useMemo(
|
||||
() =>
|
||||
[
|
||||
tokenA && tokenB
|
||||
? [
|
||||
// the direct pair
|
||||
[tokenA, tokenB],
|
||||
// token A against all bases
|
||||
...bases.map((base): [Token | undefined, Token | undefined] => [tokenA, base]),
|
||||
...bases.map((base): [Token, Token] => [tokenA, base]),
|
||||
// token B against all bases
|
||||
...bases.map((base): [Token | undefined, Token | undefined] => [tokenB, base]),
|
||||
...bases.map((base): [Token, Token] => [tokenB, base]),
|
||||
// each base against all bases
|
||||
...flatMap(bases, (base): [Token, Token][] => bases.map(otherBase => [base, otherBase]))
|
||||
...basePairs
|
||||
]
|
||||
.filter((tokens): tokens is [Token, Token] => Boolean(tokens[0] && tokens[1]))
|
||||
.filter(([t0, t1]) => t0.address !== t1.address)
|
||||
.filter(([tokenA, tokenB]) => {
|
||||
if (!chainId) return true
|
||||
const customBases = CUSTOM_BASES[chainId]
|
||||
@ -40,12 +50,13 @@ function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
|
||||
|
||||
if (!customBasesA && !customBasesB) return true
|
||||
|
||||
if (customBasesA && customBasesA.findIndex(base => tokenB.equals(base)) === -1) return false
|
||||
if (customBasesB && customBasesB.findIndex(base => tokenA.equals(base)) === -1) return false
|
||||
if (customBasesA && !customBasesA.find(base => tokenB.equals(base))) return false
|
||||
if (customBasesB && !customBasesB.find(base => tokenA.equals(base))) return false
|
||||
|
||||
return true
|
||||
}),
|
||||
[tokenA, tokenB, bases, chainId]
|
||||
})
|
||||
: [],
|
||||
[tokenA, tokenB, bases, basePairs, chainId]
|
||||
)
|
||||
|
||||
const allPairs = usePairs(allPairCombinations)
|
||||
@ -72,7 +83,6 @@ function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
|
||||
*/
|
||||
export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: Currency): Trade | null {
|
||||
const allowedPairs = useAllCommonPairs(currencyAmountIn?.currency, currencyOut)
|
||||
|
||||
return useMemo(() => {
|
||||
if (currencyAmountIn && currencyOut && allowedPairs.length > 0) {
|
||||
return (
|
||||
|
@ -2,18 +2,20 @@ import { Contract } from '@ethersproject/contracts'
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { useMemo } from 'react'
|
||||
import ENS_ABI from '../constants/abis/ens-registrar.json'
|
||||
import ENS_PUBLIC_RESOLVER_ABI from '../constants/abis/ens-public-resolver.json'
|
||||
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
|
||||
import UNISOCKS_ABI from '../constants/abis/unisocks.json'
|
||||
import ERC20_ABI from '../constants/abis/erc20.json'
|
||||
import WETH_ABI from '../constants/abis/weth.json'
|
||||
import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
|
||||
import UNISOCKS_ABI from '../constants/abis/unisocks.json'
|
||||
import WETH_ABI from '../constants/abis/weth.json'
|
||||
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
|
||||
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
|
||||
import { getContract } from '../utils'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
// returns null on errors
|
||||
function useContract(address?: string, ABI?: any, withSignerIfPossible = true): Contract | null {
|
||||
function useContract(address: string | undefined, ABI: any, withSignerIfPossible = true): Contract | null {
|
||||
const { library, account } = useActiveWeb3React()
|
||||
|
||||
return useMemo(() => {
|
||||
@ -49,6 +51,26 @@ export function useWETHContract(withSignerIfPossible?: boolean): Contract | null
|
||||
return useContract(chainId ? WETH[chainId].address : undefined, WETH_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useENSRegistrarContract(withSignerIfPossible?: boolean): Contract | null {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
let address: string | undefined
|
||||
if (chainId) {
|
||||
switch (chainId) {
|
||||
case ChainId.MAINNET:
|
||||
case ChainId.GÖRLI:
|
||||
case ChainId.ROPSTEN:
|
||||
case ChainId.RINKEBY:
|
||||
address = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'
|
||||
break
|
||||
}
|
||||
}
|
||||
return useContract(address, ENS_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useENSResolverContract(address: string | undefined, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(address, ENS_PUBLIC_RESOLVER_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
@ -1,46 +1,30 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { namehash } from 'ethers/lib/utils'
|
||||
import { useMemo } from 'react'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useENSRegistrarContract, useENSResolverContract } from './useContract'
|
||||
import useDebounce from './useDebounce'
|
||||
|
||||
/**
|
||||
* Does a lookup for an ENS name to find its address.
|
||||
*/
|
||||
export default function useENSAddress(ensName?: string | null): { loading: boolean; address: string | null } {
|
||||
const { library } = useActiveWeb3React()
|
||||
const debouncedName = useDebounce(ensName, 200)
|
||||
const ensNodeArgument = useMemo(() => {
|
||||
if (!debouncedName) return [undefined]
|
||||
try {
|
||||
return debouncedName ? [namehash(debouncedName)] : [undefined]
|
||||
} catch (error) {
|
||||
return [undefined]
|
||||
}
|
||||
}, [debouncedName])
|
||||
const registrarContract = useENSRegistrarContract(false)
|
||||
const resolverAddress = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument)
|
||||
const resolverContract = useENSResolverContract(resolverAddress.result?.[0], false)
|
||||
const addr = useSingleCallResult(resolverContract, 'addr', ensNodeArgument)
|
||||
|
||||
const [address, setAddress] = useState<{ loading: boolean; address: string | null }>({
|
||||
loading: false,
|
||||
address: null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!library || typeof ensName !== 'string') {
|
||||
setAddress({ loading: false, address: null })
|
||||
return
|
||||
} else {
|
||||
let stale = false
|
||||
setAddress({ loading: true, address: null })
|
||||
library
|
||||
.resolveName(ensName)
|
||||
.then(address => {
|
||||
if (!stale) {
|
||||
if (address) {
|
||||
setAddress({ loading: false, address })
|
||||
} else {
|
||||
setAddress({ loading: false, address: null })
|
||||
const changed = debouncedName !== ensName
|
||||
return {
|
||||
address: changed ? null : addr.result?.[0] ?? null,
|
||||
loading: changed || resolverAddress.loading || addr.loading
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setAddress({ loading: false, address: null })
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}
|
||||
}, [library, ensName])
|
||||
|
||||
return address
|
||||
}
|
||||
|
32
src/hooks/useENSContentHash.ts
Normal file
32
src/hooks/useENSContentHash.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { namehash } from 'ethers/lib/utils'
|
||||
import { useMemo } from 'react'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
import isZero from '../utils/isZero'
|
||||
import { useENSRegistrarContract, useENSResolverContract } from './useContract'
|
||||
|
||||
/**
|
||||
* Does a lookup for an ENS name to find its contenthash.
|
||||
*/
|
||||
export default function useENSContentHash(ensName?: string | null): { loading: boolean; contenthash: string | null } {
|
||||
const ensNodeArgument = useMemo(() => {
|
||||
if (!ensName) return [undefined]
|
||||
try {
|
||||
return ensName ? [namehash(ensName)] : [undefined]
|
||||
} catch (error) {
|
||||
return [undefined]
|
||||
}
|
||||
}, [ensName])
|
||||
const registrarContract = useENSRegistrarContract(false)
|
||||
const resolverAddressResult = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument)
|
||||
const resolverAddress = resolverAddressResult.result?.[0]
|
||||
const resolverContract = useENSResolverContract(
|
||||
resolverAddress && isZero(resolverAddress) ? undefined : resolverAddress,
|
||||
false
|
||||
)
|
||||
const contenthash = useSingleCallResult(resolverContract, 'contenthash', ensNodeArgument)
|
||||
|
||||
return {
|
||||
contenthash: contenthash.result?.[0] ?? null,
|
||||
loading: resolverAddressResult.loading || contenthash.loading
|
||||
}
|
||||
}
|
@ -1,49 +1,37 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { namehash } from 'ethers/lib/utils'
|
||||
import { useMemo } from 'react'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { isAddress } from '../utils'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import isZero from '../utils/isZero'
|
||||
import { useENSRegistrarContract, useENSResolverContract } from './useContract'
|
||||
import useDebounce from './useDebounce'
|
||||
|
||||
/**
|
||||
* Does a reverse lookup for an address to find its ENS name.
|
||||
* Note this is not the same as looking up an ENS name to find an address.
|
||||
*/
|
||||
export default function useENSName(address?: string): { ENSName: string | null; loading: boolean } {
|
||||
const { library } = useActiveWeb3React()
|
||||
const debouncedAddress = useDebounce(address, 200)
|
||||
const ensNodeArgument = useMemo(() => {
|
||||
if (!debouncedAddress || !isAddress(debouncedAddress)) return [undefined]
|
||||
try {
|
||||
return debouncedAddress ? [namehash(`${debouncedAddress.toLowerCase().substr(2)}.addr.reverse`)] : [undefined]
|
||||
} catch (error) {
|
||||
return [undefined]
|
||||
}
|
||||
}, [debouncedAddress])
|
||||
const registrarContract = useENSRegistrarContract(false)
|
||||
const resolverAddress = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument)
|
||||
const resolverAddressResult = resolverAddress.result?.[0]
|
||||
const resolverContract = useENSResolverContract(
|
||||
resolverAddressResult && !isZero(resolverAddressResult) ? resolverAddressResult : undefined,
|
||||
false
|
||||
)
|
||||
const name = useSingleCallResult(resolverContract, 'name', ensNodeArgument)
|
||||
|
||||
const [ENSName, setENSName] = useState<{ ENSName: string | null; loading: boolean }>({
|
||||
loading: false,
|
||||
ENSName: null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const validated = isAddress(address)
|
||||
if (!library || !validated) {
|
||||
setENSName({ loading: false, ENSName: null })
|
||||
return
|
||||
} else {
|
||||
let stale = false
|
||||
setENSName({ loading: true, ENSName: null })
|
||||
library
|
||||
.lookupAddress(validated)
|
||||
.then(name => {
|
||||
if (!stale) {
|
||||
if (name) {
|
||||
setENSName({ loading: false, ENSName: name })
|
||||
} else {
|
||||
setENSName({ loading: false, ENSName: null })
|
||||
const changed = debouncedAddress !== address
|
||||
return {
|
||||
ENSName: changed ? null : name.result?.[0] ?? null,
|
||||
loading: changed || resolverAddress.loading || name.loading
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setENSName({ loading: false, ENSName: null })
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}
|
||||
}, [library, address])
|
||||
|
||||
return ENSName
|
||||
}
|
||||
|
50
src/hooks/useFetchListCallback.ts
Normal file
50
src/hooks/useFetchListCallback.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { useCallback } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { getNetworkLibrary, NETWORK_CHAIN_ID } from '../connectors'
|
||||
import { AppDispatch } from '../state'
|
||||
import { fetchTokenList } from '../state/lists/actions'
|
||||
import getTokenList from '../utils/getTokenList'
|
||||
import resolveENSContentHash from '../utils/resolveENSContentHash'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
export function useFetchListCallback(): (listUrl: string) => Promise<TokenList> {
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const ensResolver = useCallback(
|
||||
(ensName: string) => {
|
||||
if (!library || chainId !== ChainId.MAINNET) {
|
||||
if (NETWORK_CHAIN_ID === ChainId.MAINNET) {
|
||||
const networkLibrary = getNetworkLibrary()
|
||||
if (networkLibrary) {
|
||||
return resolveENSContentHash(ensName, networkLibrary)
|
||||
}
|
||||
}
|
||||
throw new Error('Could not construct mainnet ENS resolver')
|
||||
}
|
||||
return resolveENSContentHash(ensName, library)
|
||||
},
|
||||
[chainId, library]
|
||||
)
|
||||
|
||||
return useCallback(
|
||||
async (listUrl: string) => {
|
||||
const requestId = nanoid()
|
||||
dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
|
||||
return getTokenList(listUrl, ensResolver)
|
||||
.then(tokenList => {
|
||||
dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
|
||||
return tokenList
|
||||
})
|
||||
.catch(error => {
|
||||
console.debug(`Failed to get list at url ${listUrl}`, error)
|
||||
dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
|
||||
throw error
|
||||
})
|
||||
},
|
||||
[dispatch, ensResolver]
|
||||
)
|
||||
}
|
17
src/hooks/useHttpLocations.ts
Normal file
17
src/hooks/useHttpLocations.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useMemo } from 'react'
|
||||
import contenthashToUri from '../utils/contenthashToUri'
|
||||
import { parseENSAddress } from '../utils/parseENSAddress'
|
||||
import uriToHttp from '../utils/uriToHttp'
|
||||
import useENSContentHash from './useENSContentHash'
|
||||
|
||||
export default function useHttpLocations(uri: string | undefined): string[] {
|
||||
const ens = useMemo(() => (uri ? parseENSAddress(uri) : undefined), [uri])
|
||||
const resolvedContentHash = useENSContentHash(ens?.ensName)
|
||||
return useMemo(() => {
|
||||
if (ens) {
|
||||
return resolvedContentHash.contenthash ? uriToHttp(contenthashToUri(resolvedContentHash.contenthash)) : []
|
||||
} else {
|
||||
return uri ? uriToHttp(uri) : []
|
||||
}
|
||||
}, [ens, resolvedContentHash.contenthash, uri])
|
||||
}
|
26
src/hooks/useOnClickOutside.tsx
Normal file
26
src/hooks/useOnClickOutside.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { RefObject, useEffect, useRef } from 'react'
|
||||
|
||||
export function useOnClickOutside<T extends HTMLElement>(
|
||||
node: RefObject<T | undefined>,
|
||||
handler: undefined | (() => void)
|
||||
) {
|
||||
const handlerRef = useRef<undefined | (() => void)>(handler)
|
||||
useEffect(() => {
|
||||
handlerRef.current = handler
|
||||
}, [handler])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (node.current?.contains(e.target as Node) ?? false) {
|
||||
return
|
||||
}
|
||||
if (handlerRef.current) handlerRef.current()
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [node])
|
||||
}
|
@ -1,21 +1,21 @@
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { createWeb3ReactRoot, Web3ReactProvider } from '@web3-react/core'
|
||||
import React from 'react'
|
||||
import 'inter-ui'
|
||||
import React, { StrictMode } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Provider } from 'react-redux'
|
||||
import { NetworkContextName } from './constants'
|
||||
import 'inter-ui'
|
||||
import './i18n'
|
||||
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 TransactionUpdater from './state/transactions/updater'
|
||||
import UserUpdater from './state/user/updater'
|
||||
import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
|
||||
import getLibrary from './utils/getLibrary'
|
||||
|
||||
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
|
||||
|
||||
@ -23,12 +23,6 @@ if ('ethereum' in window) {
|
||||
;(window.ethereum as any).autoRefreshOnNetworkChange = false
|
||||
}
|
||||
|
||||
function getLibrary(provider: any): Web3Provider {
|
||||
const library = new Web3Provider(provider)
|
||||
library.pollingInterval = 15000
|
||||
return library
|
||||
}
|
||||
|
||||
const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
|
||||
if (typeof GOOGLE_ANALYTICS_ID === 'string') {
|
||||
ReactGA.initialize(GOOGLE_ANALYTICS_ID)
|
||||
@ -59,21 +53,19 @@ function Updaters() {
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<>
|
||||
<StrictMode>
|
||||
<FixedGlobalStyle />
|
||||
<Web3ReactProvider getLibrary={getLibrary}>
|
||||
<Web3ProviderNetwork getLibrary={getLibrary}>
|
||||
<Provider store={store}>
|
||||
<Updaters />
|
||||
<ThemeProvider>
|
||||
<>
|
||||
<ThemedGlobalStyle />
|
||||
<App />
|
||||
</>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</Web3ProviderNetwork>
|
||||
</Web3ReactProvider>
|
||||
</>,
|
||||
</StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const BodyWrapper = styled.div<{ disabled?: boolean }>`
|
||||
export const BodyWrapper = styled.div`
|
||||
position: relative;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
@ -10,13 +10,11 @@ export const BodyWrapper = styled.div<{ disabled?: boolean }>`
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
border-radius: 30px;
|
||||
padding: 1rem;
|
||||
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
|
||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||
`
|
||||
|
||||
/**
|
||||
* The styled container element that wraps the content of most pages and the tabs.
|
||||
*/
|
||||
export default function AppBody({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) {
|
||||
return <BodyWrapper disabled={disabled}>{children}</BodyWrapper>
|
||||
export default function AppBody({ children }: { children: React.ReactNode }) {
|
||||
return <BodyWrapper>{children}</BodyWrapper>
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { SearchInput } from '../../components/SearchModal/styleds'
|
||||
import { useAllTokenV1Exchanges } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import { useDefaultTokenList } from '../../state/lists/hooks'
|
||||
import { useSelectedTokenList } from '../../state/lists/hooks'
|
||||
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
|
||||
import { BackArrow, TYPE } from '../../theme'
|
||||
import { LightCard } from '../../components/Card'
|
||||
@ -17,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 } from '../../utils'
|
||||
import { isTokenOnList } from '../../utils'
|
||||
|
||||
export default function MigrateV1() {
|
||||
const theme = useContext(ThemeContext)
|
||||
@ -28,15 +28,15 @@ export default function MigrateV1() {
|
||||
|
||||
// automatically add the search token
|
||||
const token = useToken(tokenSearch)
|
||||
const defaultTokens = useDefaultTokenList()
|
||||
const isDefault = isDefaultToken(defaultTokens, token)
|
||||
const selectedTokenListTokens = useSelectedTokenList()
|
||||
const isOnSelectedList = isTokenOnList(selectedTokenListTokens, token)
|
||||
const allTokens = useAllTokens()
|
||||
const addToken = useAddUserToken()
|
||||
useEffect(() => {
|
||||
if (token && !isDefault && !allTokens[token.address]) {
|
||||
if (token && !isOnSelectedList && !allTokens[token.address]) {
|
||||
addToken(token)
|
||||
}
|
||||
}, [token, isDefault, addToken, allTokens])
|
||||
}, [token, isOnSelectedList, addToken, allTokens])
|
||||
|
||||
// get V1 LP balances
|
||||
const V1Exchanges = useAllTokenV1Exchanges()
|
||||
|
@ -185,7 +185,7 @@ export default function PoolFinder() {
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
onDismiss={handleSearchDismiss}
|
||||
showCommonBases
|
||||
hiddenCurrency={(activeField === Fields.TOKEN0 ? currency1 : currency0) ?? undefined}
|
||||
selectedCurrency={(activeField === Fields.TOKEN0 ? currency1 : currency0) ?? undefined}
|
||||
/>
|
||||
</AppBody>
|
||||
)
|
||||
|
@ -29,6 +29,7 @@ import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { StyledInternalLink, TYPE } from '../../theme'
|
||||
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
|
||||
import { currencyId } from '../../utils/currencyId'
|
||||
import useDebouncedChangeHandler from '../../utils/useDebouncedChangeHandler'
|
||||
import { wrappedCurrency } from '../../utils/wrappedCurrency'
|
||||
import AppBody from '../AppBody'
|
||||
import { ClickableText, MaxButton, Wrapper } from '../Pool/styleds'
|
||||
@ -458,6 +459,11 @@ export default function RemoveLiquidity({
|
||||
setTxHash('')
|
||||
}, [onUserInput, txHash])
|
||||
|
||||
const [innerLiquidityPercentage, setInnerLiquidityPercentage] = useDebouncedChangeHandler(
|
||||
Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0)),
|
||||
liquidityPercentChangeCallback
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBody>
|
||||
@ -499,10 +505,7 @@ export default function RemoveLiquidity({
|
||||
</Row>
|
||||
{!showDetailed && (
|
||||
<>
|
||||
<Slider
|
||||
value={Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0))}
|
||||
onChange={liquidityPercentChangeCallback}
|
||||
/>
|
||||
<Slider value={innerLiquidityPercentage} onChange={setInnerLiquidityPercentage} />
|
||||
<RowBetween>
|
||||
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')} width="20%">
|
||||
25%
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CurrencyAmount, JSBI, Trade } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { CurrencyAmount, JSBI, Token, Trade } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { ArrowDown } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Text } from 'rebass'
|
||||
@ -17,11 +17,12 @@ import BetterTradeLink from '../../components/swap/BetterTradeLink'
|
||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||
import { ArrowWrapper, BottomGrouping, Dots, SwapCallbackError, Wrapper } from '../../components/swap/styleds'
|
||||
import TradePrice from '../../components/swap/TradePrice'
|
||||
import { TokenWarningCards } from '../../components/TokenWarningCard'
|
||||
import TokenWarningModal from '../../components/TokenWarningModal'
|
||||
|
||||
import { BETTER_TRADE_LINK_THRESHOLD, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { getTradeVersion, isTradeBetter } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useCurrency } from '../../hooks/Tokens'
|
||||
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
|
||||
import useENSAddress from '../../hooks/useENSAddress'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
@ -35,12 +36,7 @@ import {
|
||||
useSwapActionHandlers,
|
||||
useSwapState
|
||||
} from '../../state/swap/hooks'
|
||||
import {
|
||||
useExpertModeManager,
|
||||
useTokenWarningDismissal,
|
||||
useUserDeadline,
|
||||
useUserSlippageTolerance
|
||||
} from '../../state/user/hooks'
|
||||
import { useExpertModeManager, useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { maxAmountSpend } from '../../utils/maxAmountSpend'
|
||||
import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
@ -48,9 +44,23 @@ import AppBody from '../AppBody'
|
||||
import { ClickableText } from '../Pool/styleds'
|
||||
|
||||
export default function Swap() {
|
||||
useDefaultsFromURLSearch()
|
||||
const loadedUrlParams = useDefaultsFromURLSearch()
|
||||
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
// token warning stuff
|
||||
const [loadedInputCurrency, loadedOutputCurrency] = [
|
||||
useCurrency(loadedUrlParams?.inputCurrencyId),
|
||||
useCurrency(loadedUrlParams?.outputCurrencyId)
|
||||
]
|
||||
const [dismissTokenWarning, setDismissTokenWarning] = useState<boolean>(false)
|
||||
const urlLoadedTokens: Token[] = useMemo(
|
||||
() => [loadedInputCurrency, loadedOutputCurrency]?.filter((c): c is Token => c instanceof Token) ?? [],
|
||||
[loadedInputCurrency, loadedOutputCurrency]
|
||||
)
|
||||
const handleConfirmTokenWarning = useCallback(() => {
|
||||
setDismissTokenWarning(true)
|
||||
}, [])
|
||||
|
||||
const { account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
// toggle wallet when disconnected
|
||||
@ -230,11 +240,6 @@ export default function Swap() {
|
||||
(approvalSubmitted && approval === ApprovalState.APPROVED)) &&
|
||||
!(priceImpactSeverity > 3 && !isExpertMode)
|
||||
|
||||
const [dismissedToken0] = useTokenWarningDismissal(chainId, currencies[Field.INPUT])
|
||||
const [dismissedToken1] = useTokenWarningDismissal(chainId, currencies[Field.OUTPUT])
|
||||
const showWarning =
|
||||
(!dismissedToken0 && !!currencies[Field.INPUT]) || (!dismissedToken1 && !!currencies[Field.OUTPUT])
|
||||
|
||||
const handleConfirmDismiss = useCallback(() => {
|
||||
setSwapState({ showConfirm: false, tradeToConfirm, attemptingTxn, swapErrorMessage, txHash })
|
||||
// if there was a tx hash, we want to clear the input
|
||||
@ -247,10 +252,30 @@ export default function Swap() {
|
||||
setSwapState({ tradeToConfirm: trade, swapErrorMessage, txHash, attemptingTxn, showConfirm })
|
||||
}, [attemptingTxn, showConfirm, swapErrorMessage, trade, txHash])
|
||||
|
||||
const handleInputSelect = useCallback(
|
||||
inputCurrency => {
|
||||
setApprovalSubmitted(false) // reset 2 step UI for approvals
|
||||
onCurrencySelection(Field.INPUT, inputCurrency)
|
||||
},
|
||||
[onCurrencySelection]
|
||||
)
|
||||
|
||||
const handleMaxInput = useCallback(() => {
|
||||
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
|
||||
}, [maxAmountInput, onUserInput])
|
||||
|
||||
const handleOutputSelect = useCallback(outputCurrency => onCurrencySelection(Field.OUTPUT, outputCurrency), [
|
||||
onCurrencySelection
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showWarning && <TokenWarningCards currencies={currencies} />}
|
||||
<AppBody disabled={showWarning}>
|
||||
<TokenWarningModal
|
||||
isOpen={urlLoadedTokens.length > 0 && !dismissTokenWarning}
|
||||
tokens={urlLoadedTokens}
|
||||
onConfirm={handleConfirmTokenWarning}
|
||||
/>
|
||||
<AppBody>
|
||||
<SwapPoolTabs active={'swap'} />
|
||||
<Wrapper id="swap-page">
|
||||
<ConfirmSwapModal
|
||||
@ -274,13 +299,8 @@ export default function Swap() {
|
||||
showMaxButton={!atMaxAmountInput}
|
||||
currency={currencies[Field.INPUT]}
|
||||
onUserInput={handleTypeInput}
|
||||
onMax={() => {
|
||||
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
|
||||
}}
|
||||
onCurrencySelect={currency => {
|
||||
setApprovalSubmitted(false) // reset 2 step UI for approvals
|
||||
onCurrencySelection(Field.INPUT, currency)
|
||||
}}
|
||||
onMax={handleMaxInput}
|
||||
onCurrencySelect={handleInputSelect}
|
||||
otherCurrency={currencies[Field.OUTPUT]}
|
||||
id="swap-currency-input"
|
||||
/>
|
||||
@ -310,7 +330,7 @@ export default function Swap() {
|
||||
label={independentField === Field.INPUT && !showWrap ? 'To (estimated)' : 'To'}
|
||||
showMaxButton={false}
|
||||
currency={currencies[Field.OUTPUT]}
|
||||
onCurrencySelect={address => onCurrencySelection(Field.OUTPUT, address)}
|
||||
onCurrencySelect={handleOutputSelect}
|
||||
otherCurrency={currencies[Field.INPUT]}
|
||||
id="swap-currency-output"
|
||||
/>
|
||||
@ -351,7 +371,7 @@ export default function Swap() {
|
||||
Slippage Tolerance
|
||||
</ClickableText>
|
||||
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
|
||||
{allowedSlippage ? allowedSlippage / 100 : '-'}%
|
||||
{allowedSlippage / 100}%
|
||||
</ClickableText>
|
||||
</RowBetween>
|
||||
)}
|
||||
|
@ -21,5 +21,5 @@ export type PopupContent =
|
||||
export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('updateBlockNumber')
|
||||
export const toggleWalletModal = createAction<void>('toggleWalletModal')
|
||||
export const toggleSettingsMenu = createAction<void>('toggleSettingsMenu')
|
||||
export const addPopup = createAction<{ key?: string; content: PopupContent }>('addPopup')
|
||||
export const addPopup = createAction<{ key?: string; removeAfterMs?: number | null; content: PopupContent }>('addPopup')
|
||||
export const removePopup = createAction<{ key: string }>('removePopup')
|
||||
|
@ -25,6 +25,7 @@ describe('application reducer', () => {
|
||||
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 } })
|
||||
expect(list[0].removeAfterMs).toEqual(15000)
|
||||
})
|
||||
|
||||
it('replaces any existing popups with the same key', () => {
|
||||
@ -35,6 +36,7 @@ describe('application reducer', () => {
|
||||
expect(list[0].key).toEqual('abc')
|
||||
expect(list[0].show).toEqual(true)
|
||||
expect(list[0].content).toEqual({ txn: { hash: 'def', summary: 'test2', success: false } })
|
||||
expect(list[0].removeAfterMs).toEqual(15000)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
updateBlockNumber
|
||||
} from './actions'
|
||||
|
||||
type PopupList = Array<{ key: string; show: boolean; content: PopupContent }>
|
||||
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
|
||||
|
||||
export interface ApplicationState {
|
||||
blockNumber: { [chainId: number]: number }
|
||||
@ -40,12 +40,13 @@ export default createReducer(initialState, builder =>
|
||||
.addCase(toggleSettingsMenu, state => {
|
||||
state.settingsMenuOpen = !state.settingsMenuOpen
|
||||
})
|
||||
.addCase(addPopup, (state, { payload: { content, key } }) => {
|
||||
.addCase(addPopup, (state, { payload: { content, key, removeAfterMs = 15000 } }) => {
|
||||
state.popupList = (key ? state.popupList.filter(popup => popup.key !== key) : state.popupList).concat([
|
||||
{
|
||||
key: key || nanoid(),
|
||||
show: true,
|
||||
content
|
||||
content,
|
||||
removeAfterMs
|
||||
}
|
||||
])
|
||||
})
|
||||
|
@ -25,7 +25,7 @@ const store = configureStore({
|
||||
multicall,
|
||||
lists
|
||||
},
|
||||
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
|
||||
middleware: [...getDefaultMiddleware({ thunk: false }), save({ states: PERSISTED_KEYS })],
|
||||
preloadedState: load({ states: PERSISTED_KEYS })
|
||||
})
|
||||
|
||||
|
@ -1,54 +1,18 @@
|
||||
import { createAction, createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import { ActionCreatorWithPayload, createAction } 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.')
|
||||
export const fetchTokenList: Readonly<{
|
||||
pending: ActionCreatorWithPayload<{ url: string; requestId: string }>
|
||||
fulfilled: ActionCreatorWithPayload<{ url: string; tokenList: TokenList; requestId: string }>
|
||||
rejected: ActionCreatorWithPayload<{ url: string; errorMessage: string; requestId: string }>
|
||||
}> = {
|
||||
pending: createAction('lists/fetchTokenList/pending'),
|
||||
fulfilled: createAction('lists/fetchTokenList/fulfilled'),
|
||||
rejected: createAction('lists/fetchTokenList/rejected')
|
||||
}
|
||||
|
||||
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 removeList = createAction<string>('lists/removeList')
|
||||
export const selectList = createAction<string>('lists/selectList')
|
||||
export const rejectVersionUpdate = createAction<Version>('lists/rejectVersionUpdate')
|
||||
|
@ -1,18 +1,24 @@
|
||||
import { ChainId, Token } from '@uniswap/sdk'
|
||||
import { TokenInfo, TokenList } from '@uniswap/token-lists'
|
||||
import { Tags, 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'
|
||||
|
||||
type TagDetails = Tags[keyof Tags]
|
||||
export interface TagInfo extends TagDetails {
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Token instances created from token info.
|
||||
*/
|
||||
export class WrappedTokenInfo extends Token {
|
||||
public readonly tokenInfo: TokenInfo
|
||||
constructor(tokenInfo: TokenInfo) {
|
||||
public readonly tags: TagInfo[]
|
||||
constructor(tokenInfo: TokenInfo, tags: TagInfo[]) {
|
||||
super(tokenInfo.chainId, tokenInfo.address, tokenInfo.decimals, tokenInfo.symbol, tokenInfo.name)
|
||||
this.tokenInfo = tokenInfo
|
||||
this.tags = tags
|
||||
}
|
||||
public get logoURI(): string | undefined {
|
||||
return this.tokenInfo.logoURI
|
||||
@ -33,7 +39,7 @@ const EMPTY_LIST: TokenAddressMap = {
|
||||
}
|
||||
|
||||
const listCache: WeakMap<TokenList, TokenAddressMap> | null =
|
||||
'WeakMap' in window ? new WeakMap<TokenList, TokenAddressMap>() : null
|
||||
typeof WeakMap !== 'undefined' ? new WeakMap<TokenList, TokenAddressMap>() : null
|
||||
|
||||
export function listToTokenMap(list: TokenList): TokenAddressMap {
|
||||
const result = listCache?.get(list)
|
||||
@ -41,7 +47,14 @@ export function listToTokenMap(list: TokenList): TokenAddressMap {
|
||||
|
||||
const map = list.tokens.reduce<TokenAddressMap>(
|
||||
(tokenMap, tokenInfo) => {
|
||||
const token = new WrappedTokenInfo(tokenInfo)
|
||||
const tags: TagInfo[] =
|
||||
tokenInfo.tags
|
||||
?.map(tagId => {
|
||||
if (!list.tags?.[tagId]) return undefined
|
||||
return { ...list.tags[tagId], id: tagId }
|
||||
})
|
||||
?.filter((x): x is TagInfo => Boolean(x)) ?? []
|
||||
const token = new WrappedTokenInfo(tokenInfo, tags)
|
||||
if (tokenMap[token.chainId][token.address] !== undefined) throw Error('Duplicate tokens.')
|
||||
return {
|
||||
...tokenMap,
|
||||
@ -57,17 +70,38 @@ export function listToTokenMap(list: TokenList): TokenAddressMap {
|
||||
return map
|
||||
}
|
||||
|
||||
export function useTokenList(url: string): TokenAddressMap {
|
||||
export function useTokenList(url: string | undefined): TokenAddressMap {
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
return useMemo(() => {
|
||||
if (!url) return EMPTY_LIST
|
||||
const current = lists[url]?.current
|
||||
if (!current) return EMPTY_LIST
|
||||
try {
|
||||
return listToTokenMap(current)
|
||||
} catch (error) {
|
||||
console.error('Could not show token list due to error', error)
|
||||
return EMPTY_LIST
|
||||
}
|
||||
}, [lists, url])
|
||||
}
|
||||
|
||||
export function useDefaultTokenList(): TokenAddressMap {
|
||||
return useTokenList(DEFAULT_TOKEN_LIST_URL)
|
||||
export function useSelectedListUrl(): string | undefined {
|
||||
return useSelector<AppState, AppState['lists']['selectedListUrl']>(state => state.lists.selectedListUrl)
|
||||
}
|
||||
|
||||
export function useSelectedTokenList(): TokenAddressMap {
|
||||
return useTokenList(useSelectedListUrl())
|
||||
}
|
||||
|
||||
export function useSelectedListInfo(): { current: TokenList | null; pending: TokenList | null; loading: boolean } {
|
||||
const selectedUrl = useSelectedListUrl()
|
||||
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const list = selectedUrl ? listsByUrl[selectedUrl] : undefined
|
||||
return {
|
||||
current: list?.current ?? null,
|
||||
pending: list?.pendingUpdate ?? null,
|
||||
loading: list?.loadingRequestId !== null
|
||||
}
|
||||
}
|
||||
|
||||
// returns all downloaded current lists
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { createStore, Store } from 'redux'
|
||||
import { fetchTokenList, acceptListUpdate, addList } from './actions'
|
||||
import { DEFAULT_LIST_OF_LISTS, DEFAULT_TOKEN_LIST_URL } from '../../constants/lists'
|
||||
import { updateVersion } from '../user/actions'
|
||||
import { fetchTokenList, acceptListUpdate, addList, removeList, selectList } from './actions'
|
||||
import reducer, { ListsState } from './reducer'
|
||||
import UNISWAP_DEFAULT_TOKEN_LIST from '@uniswap/default-token-list'
|
||||
|
||||
const STUB_TOKEN_LIST = {
|
||||
name: '',
|
||||
@ -27,14 +30,15 @@ describe('list reducer', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {}
|
||||
byUrl: {},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchTokenList', () => {
|
||||
describe('pending', () => {
|
||||
it('sets pending', () => {
|
||||
store.dispatch(fetchTokenList.pending('request-id', 'fake-url'))
|
||||
store.dispatch(fetchTokenList.pending({ requestId: 'request-id', url: 'fake-url' }))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -43,7 +47,8 @@ describe('list reducer', () => {
|
||||
current: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
|
||||
@ -56,10 +61,11 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null,
|
||||
loadingRequestId: null
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
|
||||
store.dispatch(fetchTokenList.pending('request-id', 'fake-url'))
|
||||
store.dispatch(fetchTokenList.pending({ requestId: 'request-id', url: 'fake-url' }))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -68,14 +74,17 @@ describe('list reducer', () => {
|
||||
loadingRequestId: 'request-id',
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('fulfilled', () => {
|
||||
it('saves the list', () => {
|
||||
store.dispatch(fetchTokenList.fulfilled(STUB_TOKEN_LIST, 'request-id', 'fake-url'))
|
||||
store.dispatch(
|
||||
fetchTokenList.fulfilled({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -84,13 +93,18 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
|
||||
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'))
|
||||
store.dispatch(
|
||||
fetchTokenList.fulfilled({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
|
||||
)
|
||||
store.dispatch(
|
||||
fetchTokenList.fulfilled({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -99,14 +113,19 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
|
||||
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({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
|
||||
)
|
||||
|
||||
store.dispatch(fetchTokenList.fulfilled(PATCHED_STUB_LIST, 'request-id', 'fake-url'))
|
||||
store.dispatch(
|
||||
fetchTokenList.fulfilled({ tokenList: PATCHED_STUB_LIST, requestId: 'request-id', url: 'fake-url' })
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -115,13 +134,18 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
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({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
|
||||
)
|
||||
|
||||
store.dispatch(fetchTokenList.fulfilled(MINOR_UPDATED_STUB_LIST, 'request-id', 'fake-url'))
|
||||
store.dispatch(
|
||||
fetchTokenList.fulfilled({ tokenList: MINOR_UPDATED_STUB_LIST, requestId: 'request-id', url: 'fake-url' })
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -130,13 +154,18 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: MINOR_UPDATED_STUB_LIST
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
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({ tokenList: STUB_TOKEN_LIST, requestId: 'request-id', url: 'fake-url' })
|
||||
)
|
||||
|
||||
store.dispatch(fetchTokenList.fulfilled(MAJOR_UPDATED_STUB_LIST, 'request-id', 'fake-url'))
|
||||
store.dispatch(
|
||||
fetchTokenList.fulfilled({ tokenList: MAJOR_UPDATED_STUB_LIST, requestId: 'request-id', url: 'fake-url' })
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -145,16 +174,18 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: MAJOR_UPDATED_STUB_LIST
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('rejected', () => {
|
||||
it('no-op if not loading', () => {
|
||||
store.dispatch(fetchTokenList.rejected(new Error('abcd'), 'request-id', 'fake-url'))
|
||||
store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' }))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {}
|
||||
byUrl: {},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
|
||||
@ -167,9 +198,10 @@ describe('list reducer', () => {
|
||||
loadingRequestId: 'request-id',
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
store.dispatch(fetchTokenList.rejected(new Error('abcd'), 'request-id', 'fake-url'))
|
||||
store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' }))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -178,7 +210,8 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -195,7 +228,8 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
it('no op for existing list', () => {
|
||||
@ -207,7 +241,8 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
store.dispatch(addList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
@ -218,7 +253,8 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -233,7 +269,8 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
store.dispatch(acceptListUpdate('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
@ -244,7 +281,251 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeList', () => {
|
||||
it('deletes the list key', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
store.dispatch(removeList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
it('unselects the list if selected', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url'
|
||||
})
|
||||
store.dispatch(removeList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectList', () => {
|
||||
it('sets the selected list url', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
store.dispatch(selectList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url'
|
||||
})
|
||||
})
|
||||
it('selects if not present already', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
store.dispatch(selectList('fake-url-invalid'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
},
|
||||
'fake-url-invalid': {
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url-invalid'
|
||||
})
|
||||
})
|
||||
it('works if list already added', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
store.dispatch(selectList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateVersion', () => {
|
||||
describe('never initialized', () => {
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'https://unpkg.com/@uniswap/default-token-list@latest/uniswap-default.tokenlist.json': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
},
|
||||
'https://unpkg.com/@uniswap/default-token-list@latest': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
})
|
||||
store.dispatch(updateVersion())
|
||||
})
|
||||
|
||||
it('clears the current lists', () => {
|
||||
expect(
|
||||
store.getState().byUrl['https://unpkg.com/@uniswap/default-token-list@latest/uniswap-default.tokenlist.json']
|
||||
).toBeUndefined()
|
||||
expect(store.getState().byUrl['https://unpkg.com/@uniswap/default-token-list@latest']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('puts in all the new lists', () => {
|
||||
expect(Object.keys(store.getState().byUrl)).toEqual(DEFAULT_LIST_OF_LISTS)
|
||||
})
|
||||
it('all lists are empty', () => {
|
||||
const s = store.getState()
|
||||
Object.keys(s.byUrl).forEach(url => {
|
||||
if (url === DEFAULT_TOKEN_LIST_URL) {
|
||||
expect(s.byUrl[url]).toEqual({
|
||||
error: null,
|
||||
current: UNISWAP_DEFAULT_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
})
|
||||
} else {
|
||||
expect(s.byUrl[url]).toEqual({
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
it('sets initialized lists', () => {
|
||||
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS)
|
||||
})
|
||||
})
|
||||
describe('initialized with a different set of lists', () => {
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'https://unpkg.com/@uniswap/default-token-list@latest/uniswap-default.tokenlist.json': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
},
|
||||
'https://unpkg.com/@uniswap/default-token-list@latest': {
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined,
|
||||
lastInitializedDefaultListOfLists: ['https://unpkg.com/@uniswap/default-token-list@latest']
|
||||
})
|
||||
store.dispatch(updateVersion())
|
||||
})
|
||||
|
||||
it('does not remove lists not in last initialized list of lists', () => {
|
||||
expect(
|
||||
store.getState().byUrl['https://unpkg.com/@uniswap/default-token-list@latest/uniswap-default.tokenlist.json']
|
||||
).toEqual({
|
||||
error: null,
|
||||
current: STUB_TOKEN_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
})
|
||||
})
|
||||
it('removes lists in the last initialized list of lists', () => {
|
||||
expect(store.getState().byUrl['https://unpkg.com/@uniswap/default-token-list@latest']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('adds all the lists in the default list of lists', () => {
|
||||
expect(Object.keys(store.getState().byUrl)).toContain(DEFAULT_TOKEN_LIST_URL)
|
||||
})
|
||||
|
||||
it('each of those initialized lists is empty', () => {
|
||||
const byUrl = store.getState().byUrl
|
||||
// note we don't expect the uniswap default list to be prepopulated
|
||||
// this is ok.
|
||||
Object.keys(byUrl).forEach(url => {
|
||||
if (url !== 'https://unpkg.com/@uniswap/default-token-list@latest/uniswap-default.tokenlist.json') {
|
||||
expect(byUrl[url]).toEqual({
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('sets initialized lists', () => {
|
||||
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { getVersionUpgrade, VersionUpgrade } from '@uniswap/token-lists'
|
||||
import { TokenList } from '@uniswap/token-lists/dist/types'
|
||||
import { DEFAULT_LIST_OF_LISTS, DEFAULT_TOKEN_LIST_URL } from '../../constants/lists'
|
||||
import { updateVersion } from '../user/actions'
|
||||
import { acceptListUpdate, addList, fetchTokenList } from './actions'
|
||||
import { acceptListUpdate, addList, fetchTokenList, removeList, selectList } from './actions'
|
||||
import UNISWAP_DEFAULT_LIST from '@uniswap/default-token-list'
|
||||
|
||||
export interface ListsState {
|
||||
readonly byUrl: {
|
||||
@ -13,15 +15,40 @@ export interface ListsState {
|
||||
readonly error: string | null
|
||||
}
|
||||
}
|
||||
// this contains the default list of lists from the last time the updateVersion was called, i.e. the app was reloaded
|
||||
readonly lastInitializedDefaultListOfLists?: string[]
|
||||
readonly selectedListUrl: string | undefined
|
||||
}
|
||||
|
||||
const NEW_LIST_STATE: ListsState['byUrl'][string] = {
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
|
||||
type Mutable<T> = { -readonly [P in keyof T]: T[P] extends ReadonlyArray<infer U> ? U[] : T[P] }
|
||||
|
||||
const initialState: ListsState = {
|
||||
byUrl: {}
|
||||
lastInitializedDefaultListOfLists: DEFAULT_LIST_OF_LISTS,
|
||||
byUrl: {
|
||||
...DEFAULT_LIST_OF_LISTS.reduce<Mutable<ListsState['byUrl']>>((memo, listUrl) => {
|
||||
memo[listUrl] = NEW_LIST_STATE
|
||||
return memo
|
||||
}, {}),
|
||||
[DEFAULT_TOKEN_LIST_URL]: {
|
||||
error: null,
|
||||
current: UNISWAP_DEFAULT_LIST,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
builder
|
||||
.addCase(fetchTokenList.pending, (state, { meta: { arg: url, requestId } }) => {
|
||||
.addCase(fetchTokenList.pending, (state, { payload: { requestId, url } }) => {
|
||||
state.byUrl[url] = {
|
||||
current: null,
|
||||
pendingUpdate: null,
|
||||
@ -30,13 +57,15 @@ export default createReducer(initialState, builder =>
|
||||
error: null
|
||||
}
|
||||
})
|
||||
.addCase(fetchTokenList.fulfilled, (state, { payload: tokenList, meta: { arg: url } }) => {
|
||||
.addCase(fetchTokenList.fulfilled, (state, { payload: { requestId, tokenList, url } }) => {
|
||||
const current = state.byUrl[url]?.current
|
||||
const loadingRequestId = state.byUrl[url]?.loadingRequestId
|
||||
|
||||
// no-op if update does nothing
|
||||
if (current) {
|
||||
const type = getVersionUpgrade(current.version, tokenList.version)
|
||||
if (type === VersionUpgrade.NONE) return
|
||||
const upgradeType = getVersionUpgrade(current.version, tokenList.version)
|
||||
if (upgradeType === VersionUpgrade.NONE) return
|
||||
if (loadingRequestId === null || loadingRequestId === requestId) {
|
||||
state.byUrl[url] = {
|
||||
...state.byUrl[url],
|
||||
loadingRequestId: null,
|
||||
@ -44,6 +73,7 @@ export default createReducer(initialState, builder =>
|
||||
current: current,
|
||||
pendingUpdate: tokenList
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.byUrl[url] = {
|
||||
...state.byUrl[url],
|
||||
@ -54,7 +84,7 @@ export default createReducer(initialState, builder =>
|
||||
}
|
||||
}
|
||||
})
|
||||
.addCase(fetchTokenList.rejected, (state, { error, meta: { requestId, arg: url } }) => {
|
||||
.addCase(fetchTokenList.rejected, (state, { payload: { url, requestId, errorMessage } }) => {
|
||||
if (state.byUrl[url]?.loadingRequestId !== requestId) {
|
||||
// no-op since it's not the latest request
|
||||
return
|
||||
@ -63,19 +93,29 @@ export default createReducer(initialState, builder =>
|
||||
state.byUrl[url] = {
|
||||
...state.byUrl[url],
|
||||
loadingRequestId: null,
|
||||
error: error.message ?? 'Unknown error',
|
||||
error: errorMessage,
|
||||
current: null,
|
||||
pendingUpdate: null
|
||||
}
|
||||
})
|
||||
.addCase(selectList, (state, { payload: url }) => {
|
||||
state.selectedListUrl = url
|
||||
// automatically adds list
|
||||
if (!state.byUrl[url]) {
|
||||
state.byUrl[url] = NEW_LIST_STATE
|
||||
}
|
||||
})
|
||||
.addCase(addList, (state, { payload: url }) => {
|
||||
if (!state.byUrl[url]) {
|
||||
state.byUrl[url] = {
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null,
|
||||
current: null,
|
||||
error: null
|
||||
state.byUrl[url] = NEW_LIST_STATE
|
||||
}
|
||||
})
|
||||
.addCase(removeList, (state, { payload: url }) => {
|
||||
if (state.byUrl[url]) {
|
||||
delete state.byUrl[url]
|
||||
}
|
||||
if (state.selectedListUrl === url) {
|
||||
state.selectedListUrl = Object.keys(state.byUrl)[0]
|
||||
}
|
||||
})
|
||||
.addCase(acceptListUpdate, (state, { payload: url }) => {
|
||||
@ -89,6 +129,30 @@ export default createReducer(initialState, builder =>
|
||||
}
|
||||
})
|
||||
.addCase(updateVersion, state => {
|
||||
delete state.byUrl['https://unpkg.com/@uniswap/default-token-list@latest/uniswap-default.tokenlist.json']
|
||||
// state loaded from localStorage, but new lists have never been initialized
|
||||
if (!state.lastInitializedDefaultListOfLists) {
|
||||
state.byUrl = initialState.byUrl
|
||||
state.selectedListUrl = undefined
|
||||
} else if (state.lastInitializedDefaultListOfLists) {
|
||||
const lastInitializedSet = state.lastInitializedDefaultListOfLists.reduce<Set<string>>(
|
||||
(s, l) => s.add(l),
|
||||
new Set()
|
||||
)
|
||||
const newListOfListsSet = DEFAULT_LIST_OF_LISTS.reduce<Set<string>>((s, l) => s.add(l), new Set())
|
||||
|
||||
DEFAULT_LIST_OF_LISTS.forEach(listUrl => {
|
||||
if (!lastInitializedSet.has(listUrl)) {
|
||||
state.byUrl[listUrl] = NEW_LIST_STATE
|
||||
}
|
||||
})
|
||||
|
||||
state.lastInitializedDefaultListOfLists.forEach(listUrl => {
|
||||
if (!newListOfListsSet.has(listUrl)) {
|
||||
delete state.byUrl[listUrl]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
state.lastInitializedDefaultListOfLists = DEFAULT_LIST_OF_LISTS
|
||||
})
|
||||
)
|
||||
|
@ -1,36 +1,43 @@
|
||||
import { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists'
|
||||
import { useEffect } from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { DEFAULT_TOKEN_LIST_URL } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
|
||||
import { addPopup } from '../application/actions'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { acceptListUpdate, addList, fetchTokenList } from './actions'
|
||||
import { acceptListUpdate } from './actions'
|
||||
|
||||
export default function Updater(): null {
|
||||
const { library } = useActiveWeb3React()
|
||||
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])
|
||||
const isWindowVisible = useIsWindowVisible()
|
||||
|
||||
// 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])
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
const fetchAllListsCallback = useCallback(() => {
|
||||
if (!isWindowVisible) return
|
||||
Object.keys(lists).forEach(url =>
|
||||
fetchList(url).catch(error => console.debug('interval list fetching error', error))
|
||||
)
|
||||
}, [fetchList, isWindowVisible, lists])
|
||||
|
||||
// fetch all lists every 10 minutes, but only after we initialize library
|
||||
useInterval(fetchAllListsCallback, library ? 1000 * 60 * 10 : null)
|
||||
|
||||
// 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)
|
||||
fetchList(listUrl).catch(error => console.debug('list added fetching error', error))
|
||||
}
|
||||
})
|
||||
}, [dispatch, lists])
|
||||
}, [dispatch, fetchList, library, lists])
|
||||
|
||||
// automatically update lists if versions are minor/patch
|
||||
useEffect(() => {
|
||||
@ -43,7 +50,6 @@ export default function Updater(): null {
|
||||
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) {
|
||||
@ -68,21 +74,21 @@ export default function Updater(): null {
|
||||
}
|
||||
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
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// )
|
||||
case VersionUpgrade.MAJOR:
|
||||
dispatch(
|
||||
addPopup({
|
||||
key: listUrl,
|
||||
content: {
|
||||
listUpdate: {
|
||||
listUrl,
|
||||
auto: false,
|
||||
oldList: list.current,
|
||||
newList: list.pendingUpdate
|
||||
}
|
||||
},
|
||||
removeAfterMs: null
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -3,7 +3,7 @@ import { Version } from '../../hooks/useToggledVersion'
|
||||
import { parseUnits } from '@ethersproject/units'
|
||||
import { Currency, CurrencyAmount, ETHER, JSBI, Token, TokenAmount, Trade } from '@uniswap/sdk'
|
||||
import { ParsedQs } from 'qs'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useV1Trade } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
@ -246,10 +246,15 @@ export function queryParametersToSwapState(parsedQs: ParsedQs): SwapState {
|
||||
}
|
||||
|
||||
// updates the swap state to use the defaults for a given network
|
||||
export function useDefaultsFromURLSearch() {
|
||||
export function useDefaultsFromURLSearch():
|
||||
| { inputCurrencyId: string | undefined; outputCurrencyId: string | undefined }
|
||||
| undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const parsedQs = useParsedQueryString()
|
||||
const [result, setResult] = useState<
|
||||
{ inputCurrencyId: string | undefined; outputCurrencyId: string | undefined } | undefined
|
||||
>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!chainId) return
|
||||
@ -264,6 +269,10 @@ export function useDefaultsFromURLSearch() {
|
||||
recipient: parsed.recipient
|
||||
})
|
||||
)
|
||||
|
||||
setResult({ inputCurrencyId: parsed[Field.INPUT].currencyId, outputCurrencyId: parsed[Field.OUTPUT].currencyId })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch, chainId])
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -27,4 +27,3 @@ export const addSerializedPair = createAction<{ serializedPair: SerializedPair }
|
||||
export const removeSerializedPair = createAction<{ chainId: number; tokenAAddress: string; tokenBAddress: string }>(
|
||||
'removeSerializedPair'
|
||||
)
|
||||
export const dismissTokenWarning = createAction<{ chainId: number; tokenAddress: string }>('dismissTokenWarning')
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ChainId, Pair, Token, Currency } from '@uniswap/sdk'
|
||||
import { ChainId, Pair, Token } from '@uniswap/sdk'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
|
||||
@ -10,7 +10,6 @@ import { AppDispatch, AppState } from '../index'
|
||||
import {
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
dismissTokenWarning,
|
||||
removeSerializedToken,
|
||||
SerializedPair,
|
||||
SerializedToken,
|
||||
@ -19,8 +18,6 @@ import {
|
||||
updateUserExpertMode,
|
||||
updateUserSlippageTolerance
|
||||
} from './actions'
|
||||
import { useDefaultTokenList } from '../lists/hooks'
|
||||
import { isDefaultToken } from '../../utils'
|
||||
|
||||
function serializeToken(token: Token): SerializedToken {
|
||||
return {
|
||||
@ -163,36 +160,6 @@ export function usePairAdder(): (pair: Pair) => void {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a token warning has been dismissed and a callback to dismiss it,
|
||||
* iff it has not already been dismissed and is a valid token.
|
||||
*/
|
||||
export function useTokenWarningDismissal(chainId?: number, token?: Currency): [boolean, null | (() => void)] {
|
||||
const dismissalState = useSelector<AppState, AppState['user']['dismissedTokenWarnings']>(
|
||||
state => state.user.dismissedTokenWarnings
|
||||
)
|
||||
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
// get default list, mark as dismissed if on list
|
||||
const defaultList = useDefaultTokenList()
|
||||
const isDefault = isDefaultToken(defaultList, token)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!chainId || !token) return [false, null]
|
||||
|
||||
const dismissed: boolean =
|
||||
token instanceof Token ? dismissalState?.[chainId]?.[token.address] === true || isDefault : true
|
||||
|
||||
const callback =
|
||||
dismissed || !(token instanceof Token)
|
||||
? null
|
||||
: () => dispatch(dismissTokenWarning({ chainId, tokenAddress: token.address }))
|
||||
|
||||
return [dismissed, callback]
|
||||
}, [chainId, token, dismissalState, isDefault, dispatch])
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two tokens return the liquidity token that represents its liquidity shares
|
||||
* @param tokenA one of the two tokens
|
||||
|
@ -3,7 +3,6 @@ import { createReducer } from '@reduxjs/toolkit'
|
||||
import {
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
dismissTokenWarning,
|
||||
removeSerializedPair,
|
||||
removeSerializedToken,
|
||||
SerializedPair,
|
||||
@ -39,13 +38,6 @@ export interface UserState {
|
||||
}
|
||||
}
|
||||
|
||||
// the token warnings that the user has dismissed
|
||||
dismissedTokenWarnings?: {
|
||||
[chainId: number]: {
|
||||
[tokenAddress: string]: true
|
||||
}
|
||||
}
|
||||
|
||||
pairs: {
|
||||
[chainId: number]: {
|
||||
// keyed by token0Address:token1Address
|
||||
@ -75,11 +67,13 @@ export default createReducer(initialState, builder =>
|
||||
builder
|
||||
.addCase(updateVersion, state => {
|
||||
// slippage isnt being tracked in local storage, reset to default
|
||||
// noinspection SuspiciousTypeOfGuard
|
||||
if (typeof state.userSlippageTolerance !== 'number') {
|
||||
state.userSlippageTolerance = INITIAL_ALLOWED_SLIPPAGE
|
||||
}
|
||||
|
||||
// deadline isnt being tracked in local storage, reset to default
|
||||
// noinspection SuspiciousTypeOfGuard
|
||||
if (typeof state.userDeadline !== 'number') {
|
||||
state.userDeadline = DEFAULT_DEADLINE_FROM_NOW
|
||||
}
|
||||
@ -116,11 +110,6 @@ export default createReducer(initialState, builder =>
|
||||
delete state.tokens[chainId][address]
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(dismissTokenWarning, (state, { payload: { chainId, tokenAddress } }) => {
|
||||
state.dismissedTokenWarnings = state.dismissedTokenWarnings ?? {}
|
||||
state.dismissedTokenWarnings[chainId] = state.dismissedTokenWarnings[chainId] ?? {}
|
||||
state.dismissedTokenWarnings[chainId][tokenAddress] = true
|
||||
})
|
||||
.addCase(addSerializedPair, (state, { payload: { serializedPair } }) => {
|
||||
if (
|
||||
serializedPair.token0.chainId === serializedPair.token1.chainId &&
|
||||
|
@ -40,22 +40,22 @@ export const CloseIcon = styled(X)<{ onClick: () => void }>`
|
||||
`
|
||||
|
||||
// A button that triggers some onClick result, but looks like a link.
|
||||
export const LinkStyledButton = styled.button`
|
||||
export const LinkStyledButton = styled.button<{ disabled?: boolean }>`
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
background: none;
|
||||
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')};
|
||||
color: ${({ theme, disabled }) => (disabled ? theme.text2 : theme.primary1)};
|
||||
font-weight: 500;
|
||||
|
||||
:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration: ${({ disabled }) => (disabled ? null : 'underline')};
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
text-decoration: ${({ disabled }) => (disabled ? null : 'underline')};
|
||||
}
|
||||
|
||||
:active {
|
||||
|
@ -52,10 +52,10 @@ export function colors(darkMode: boolean): Colors {
|
||||
bg2: darkMode ? '#2C2F36' : '#F7F8FA',
|
||||
bg3: darkMode ? '#40444F' : '#EDEEF2',
|
||||
bg4: darkMode ? '#565A69' : '#CED0D9',
|
||||
bg5: darkMode ? '#565A69' : '#888D9B',
|
||||
bg5: darkMode ? '#6C7284' : '#888D9B',
|
||||
|
||||
//specialty colors
|
||||
modalBG: darkMode ? 'rgba(0,0,0,42.5)' : 'rgba(0,0,0,0.3)',
|
||||
modalBG: darkMode ? 'rgba(0,0,0,.425)' : 'rgba(0,0,0,0.3)',
|
||||
advancedBG: darkMode ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.6)',
|
||||
|
||||
//primary colors
|
||||
|
4
src/utils/content-hash.d.ts
vendored
Normal file
4
src/utils/content-hash.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module 'content-hash' {
|
||||
declare function decode(x: string): string
|
||||
declare function getCodec(x: string): string
|
||||
}
|
21
src/utils/contenthashToUri.test.skip.ts
Normal file
21
src/utils/contenthashToUri.test.skip.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import contenthashToUri, { hexToUint8Array } from './contenthashToUri'
|
||||
|
||||
// this test is skipped for now because importing CID results in
|
||||
// TypeError: TextDecoder is not a constructor
|
||||
|
||||
describe('#contenthashToUri', () => {
|
||||
it('1inch.tokens.eth contenthash', () => {
|
||||
expect(contenthashToUri('0xe3010170122013e051d1cfff20606de36845d4fe28deb9861a319a5bc8596fa4e610e8803918')).toEqual(
|
||||
'ipfs://QmPgEqyV3m8SB52BS2j2mJpu9zGprhj2BGCHtRiiw2fdM1'
|
||||
)
|
||||
})
|
||||
it('uniswap.eth contenthash', () => {
|
||||
expect(contenthashToUri('0xe5010170000f6170702e756e69737761702e6f7267')).toEqual('ipns://app.uniswap.org')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hexToUint8Array', () => {
|
||||
it('common case', () => {
|
||||
expect(hexToUint8Array('0x010203fdfeff')).toEqual(new Uint8Array([1, 2, 3, 253, 254, 255]))
|
||||
})
|
||||
})
|
43
src/utils/contenthashToUri.ts
Normal file
43
src/utils/contenthashToUri.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import CID from 'cids'
|
||||
import { getCodec, rmPrefix } from 'multicodec'
|
||||
import { decode, toB58String } from 'multihashes'
|
||||
|
||||
export function hexToUint8Array(hex: string): Uint8Array {
|
||||
hex = hex.startsWith('0x') ? hex.substr(2) : hex
|
||||
if (hex.length % 2 !== 0) throw new Error('hex must have length that is multiple of 2')
|
||||
const arr = new Uint8Array(hex.length / 2)
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i] = parseInt(hex.substr(i * 2, 2), 16)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
const UTF_8_DECODER = new TextDecoder()
|
||||
|
||||
/**
|
||||
* Returns the URI representation of the content hash for supported codecs
|
||||
* @param contenthash to decode
|
||||
*/
|
||||
export default function contenthashToUri(contenthash: string): string {
|
||||
const buff = hexToUint8Array(contenthash)
|
||||
const codec = getCodec(buff as Buffer) // the typing is wrong for @types/multicodec
|
||||
switch (codec) {
|
||||
case 'ipfs-ns': {
|
||||
const data = rmPrefix(buff as Buffer)
|
||||
const cid = new CID(data)
|
||||
return `ipfs://${toB58String(cid.multihash)}`
|
||||
}
|
||||
case 'ipns-ns': {
|
||||
const data = rmPrefix(buff as Buffer)
|
||||
const cid = new CID(data)
|
||||
const multihash = decode(cid.multihash)
|
||||
if (multihash.name === 'identity') {
|
||||
return `ipns://${UTF_8_DECODER.decode(multihash.digest).trim()}`
|
||||
} else {
|
||||
return `ipns://${toB58String(cid.multihash)}`
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unrecognized codec: ${codec}`)
|
||||
}
|
||||
}
|
7
src/utils/getLibrary.ts
Normal file
7
src/utils/getLibrary.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
|
||||
export default function getLibrary(provider: any): Web3Provider {
|
||||
const library = new Web3Provider(provider)
|
||||
library.pollingInterval = 15000
|
||||
return library
|
||||
}
|
69
src/utils/getTokenList.ts
Normal file
69
src/utils/getTokenList.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import schema from '@uniswap/token-lists/src/tokenlist.schema.json'
|
||||
import Ajv from 'ajv'
|
||||
import contenthashToUri from './contenthashToUri'
|
||||
import { parseENSAddress } from './parseENSAddress'
|
||||
import uriToHttp from './uriToHttp'
|
||||
|
||||
const tokenListValidator = new Ajv({ allErrors: true }).compile(schema)
|
||||
|
||||
/**
|
||||
* Contains the logic for resolving a list URL to a validated token list
|
||||
* @param listUrl list url
|
||||
* @param resolveENSContentHash resolves an ens name to a contenthash
|
||||
*/
|
||||
export default async function getTokenList(
|
||||
listUrl: string,
|
||||
resolveENSContentHash: (ensName: string) => Promise<string>
|
||||
): Promise<TokenList> {
|
||||
const parsedENS = parseENSAddress(listUrl)
|
||||
let urls: string[]
|
||||
if (parsedENS) {
|
||||
let contentHashUri
|
||||
try {
|
||||
contentHashUri = await resolveENSContentHash(parsedENS.ensName)
|
||||
} catch (error) {
|
||||
console.debug(`Failed to resolve ENS name: ${parsedENS.ensName}`, error)
|
||||
throw new Error(`Failed to resolve ENS name: ${parsedENS.ensName}`)
|
||||
}
|
||||
let translatedUri
|
||||
try {
|
||||
translatedUri = contenthashToUri(contentHashUri)
|
||||
} catch (error) {
|
||||
console.debug('Failed to translate contenthash to URI', contentHashUri)
|
||||
throw new Error(`Failed to translate contenthash to URI: ${contentHashUri}`)
|
||||
}
|
||||
urls = uriToHttp(`${translatedUri}${parsedENS.ensPath ?? ''}`)
|
||||
} else {
|
||||
urls = uriToHttp(listUrl)
|
||||
}
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const url = urls[i]
|
||||
const isLast = i === urls.length - 1
|
||||
let response
|
||||
try {
|
||||
response = await fetch(url)
|
||||
} catch (error) {
|
||||
console.debug('Failed to fetch list', listUrl, error)
|
||||
if (isLast) throw new Error(`Failed to download list ${listUrl}`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (isLast) throw new Error(`Failed to download list ${listUrl}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
if (!tokenListValidator(json)) {
|
||||
const validationErrors: string =
|
||||
tokenListValidator.errors?.reduce<string>((memo, error) => {
|
||||
const add = `${error.dataPath} ${error.message ?? ''}`
|
||||
return memo.length > 0 ? `${memo}; ${add}` : `${add}`
|
||||
}, '') ?? 'unknown error'
|
||||
throw new Error(`Token list failed validation: ${validationErrors}`)
|
||||
}
|
||||
return json
|
||||
}
|
||||
throw new Error('Unrecognized list URL protocol.')
|
||||
}
|
@ -99,7 +99,7 @@ export function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||
}
|
||||
|
||||
export function isDefaultToken(defaultTokens: TokenAddressMap, currency?: Currency): boolean {
|
||||
export function isTokenOnList(defaultTokens: TokenAddressMap, currency?: Currency): boolean {
|
||||
if (currency === ETHER) return true
|
||||
return Boolean(currency instanceof Token && defaultTokens[currency.chainId]?.[currency.address])
|
||||
}
|
||||
|
5
src/utils/listVersionLabel.ts
Normal file
5
src/utils/listVersionLabel.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Version } from '@uniswap/token-lists'
|
||||
|
||||
export default function listVersionLabel(version: Version): string {
|
||||
return `v${version.major}.${version.minor}.${version.patch}`
|
||||
}
|
4
src/utils/multihashes.d.ts
vendored
Normal file
4
src/utils/multihashes.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module 'multihashes' {
|
||||
declare function decode(buff: Uint8Array): { code: number; name: string; length: number; digest: Uint8Array }
|
||||
declare function toB58String(hash: Uint8Array): string
|
||||
}
|
14
src/utils/parseENSAddress.test.ts
Normal file
14
src/utils/parseENSAddress.test.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { parseENSAddress } from './parseENSAddress'
|
||||
|
||||
describe('parseENSAddress', () => {
|
||||
it('test cases', () => {
|
||||
expect(parseENSAddress('hello.eth')).toEqual({ ensName: 'hello.eth', ensPath: undefined })
|
||||
expect(parseENSAddress('hello.eth/')).toEqual({ ensName: 'hello.eth', ensPath: '/' })
|
||||
expect(parseENSAddress('hello.world.eth/')).toEqual({ ensName: 'hello.world.eth', ensPath: '/' })
|
||||
expect(parseENSAddress('hello.world.eth/abcdef')).toEqual({ ensName: 'hello.world.eth', ensPath: '/abcdef' })
|
||||
expect(parseENSAddress('abso.lutely')).toEqual(undefined)
|
||||
expect(parseENSAddress('abso.lutely.eth')).toEqual({ ensName: 'abso.lutely.eth', ensPath: undefined })
|
||||
expect(parseENSAddress('eth')).toEqual(undefined)
|
||||
expect(parseENSAddress('eth/hello-world')).toEqual(undefined)
|
||||
})
|
||||
})
|
7
src/utils/parseENSAddress.ts
Normal file
7
src/utils/parseENSAddress.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const ENS_NAME_REGEX = /^(([a-zA-Z0-9]+\.)+)eth(\/.*)?$/
|
||||
|
||||
export function parseENSAddress(ensAddress: string): { ensName: string; ensPath: string | undefined } | undefined {
|
||||
const match = ensAddress.match(ENS_NAME_REGEX)
|
||||
if (!match) return
|
||||
return { ensName: `${match[1].toLowerCase()}eth`, ensPath: match[3] }
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* Helper type that returns the props type of another component, excluding
|
||||
* any of the keys passed as the optional second argument.
|
||||
*/
|
||||
type PropsOfExcluding<TComponent, TExcludingProps = void> = TComponent extends React.ComponentType<infer P>
|
||||
? TExcludingProps extends string | number | symbol
|
||||
? Omit<P, TExcludingProps>
|
||||
: P
|
||||
: unknown
|
||||
|
||||
export default PropsOfExcluding
|
67
src/utils/resolveENSContentHash.ts
Normal file
67
src/utils/resolveENSContentHash.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { Provider } from '@ethersproject/abstract-provider'
|
||||
import { namehash } from 'ethers/lib/utils'
|
||||
|
||||
const REGISTRAR_ABI = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: 'node',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'resolver',
|
||||
outputs: [
|
||||
{
|
||||
name: 'resolverAddress',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
const REGISTRAR_ADDRESS = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'
|
||||
|
||||
const RESOLVER_ABI = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'bytes32',
|
||||
name: 'node',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'contenthash',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'bytes',
|
||||
name: '',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
// cache the resolver contracts since most of them are the public resolver
|
||||
function resolverContract(resolverAddress: string, provider: Provider): Contract {
|
||||
return new Contract(resolverAddress, RESOLVER_ABI, provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and decodes the result of an ENS contenthash lookup on mainnet to a URI
|
||||
* @param ensName to resolve
|
||||
* @param provider provider to use to fetch the data
|
||||
*/
|
||||
export default async function resolveENSContentHash(ensName: string, provider: Provider): Promise<string> {
|
||||
const ensRegistrarContract = new Contract(REGISTRAR_ADDRESS, REGISTRAR_ABI, provider)
|
||||
const hash = namehash(ensName)
|
||||
const resolverAddress = await ensRegistrarContract.resolver(hash)
|
||||
return resolverContract(resolverAddress, provider).contenthash(hash)
|
||||
}
|
@ -2,7 +2,7 @@ import uriToHttp from './uriToHttp'
|
||||
|
||||
describe('uriToHttp', () => {
|
||||
it('returns .eth.link for ens names', () => {
|
||||
expect(uriToHttp('t2crtokens.eth')).toEqual(['https://t2crtokens.eth.link'])
|
||||
expect(uriToHttp('t2crtokens.eth')).toEqual([])
|
||||
})
|
||||
it('returns https first for http', () => {
|
||||
expect(uriToHttp('http://test.com')).toEqual(['https://test.com', 'http://test.com'])
|
||||
|
@ -1,27 +1,21 @@
|
||||
/**
|
||||
* 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
|
||||
* Given a URI that may be ipfs, ipns, http, or https protocol, return the fetch-able http(s) URLs for the same content
|
||||
* @param uri to convert to fetch-able 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:') {
|
||||
const protocol = uri.split(':')[0].toLowerCase()
|
||||
switch (protocol) {
|
||||
case 'https':
|
||||
return [uri]
|
||||
} else if (parsed.protocol === 'ipfs:') {
|
||||
const hash = parsed.href.match(/^ipfs:(\/\/)?(.*)$/)?.[2]
|
||||
case 'http':
|
||||
return ['https' + uri.substr(4), uri]
|
||||
case 'ipfs':
|
||||
const hash = uri.match(/^ipfs:(\/\/)?(.*)$/i)?.[2]
|
||||
return [`https://cloudflare-ipfs.com/ipfs/${hash}/`, `https://ipfs.io/ipfs/${hash}/`]
|
||||
} else if (parsed.protocol === 'ipns:') {
|
||||
const name = parsed.href.match(/^ipns:(\/\/)?(.*)$/)?.[2]
|
||||
case 'ipns':
|
||||
const name = uri.match(/^ipns:(\/\/)?(.*)$/i)?.[2]
|
||||
return [`https://cloudflare-ipfs.com/ipns/${name}/`, `https://ipfs.io/ipns/${name}/`]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
} catch (error) {
|
||||
if (uri.toLowerCase().endsWith('.eth')) {
|
||||
return [`https://${uri.toLowerCase()}.link`]
|
||||
}
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
40
src/utils/useDebouncedChangeHandler.tsx
Normal file
40
src/utils/useDebouncedChangeHandler.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Easy way to debounce the handling of a rapidly changing value, e.g. a changing slider input
|
||||
* @param value value that is rapidly changing
|
||||
* @param onChange change handler that should receive the debounced updates to the value
|
||||
* @param debouncedMs how long we should wait for changes to be applied
|
||||
*/
|
||||
export default function useDebouncedChangeHandler<T>(
|
||||
value: T,
|
||||
onChange: (newValue: T) => void,
|
||||
debouncedMs = 100
|
||||
): [T, (value: T) => void] {
|
||||
const [inner, setInner] = useState<T>(() => value)
|
||||
const timer = useRef<ReturnType<typeof setTimeout>>()
|
||||
|
||||
const onChangeInner = useCallback(
|
||||
(newValue: T) => {
|
||||
setInner(newValue)
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
}
|
||||
timer.current = setTimeout(() => {
|
||||
onChange(newValue)
|
||||
timer.current = undefined
|
||||
}, debouncedMs)
|
||||
},
|
||||
[debouncedMs, onChange]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
timer.current = undefined
|
||||
}
|
||||
setInner(value)
|
||||
}, [value])
|
||||
|
||||
return [inner, onChangeInner]
|
||||
}
|
99
yarn.lock
99
yarn.lock
@ -2326,6 +2326,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
|
||||
|
||||
"@types/multicodec@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/multicodec/-/multicodec-1.0.0.tgz#9c9c2df84ea5006c65a048873600f71c4565a397"
|
||||
integrity sha512-UZkJT3rb8AfT2S1bTk7Gj+1wP9GJQ4zSnHDycRxEiI4yPOn47s5rSK86w/EFHvnNBhsu3zl+XNbTnBcxBd9dAQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "14.0.26"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.26.tgz#22a3b8a46510da8944b67bfc27df02c34a35331c"
|
||||
@ -2404,6 +2411,13 @@
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-virtualized-auto-sizer@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.0.tgz#fc32f30a8dab527b5816f3a757e1e1d040c8f272"
|
||||
integrity sha512-NMErdIdSnm2j/7IqMteRiRvRulpjoELnXWUwdbucYCz84xG9PHcoOrr7QfXwB/ku7wd6egiKFrzt/+QK4Imeeg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-window@^1.8.2":
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.2.tgz#a5a6b2762ce73ffaab7911ee1397cf645f2459fe"
|
||||
@ -2550,6 +2564,11 @@
|
||||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@uniswap/default-token-list@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-1.3.0.tgz#fd40e14165b31ff098b031b58ce8ca5ab81b3247"
|
||||
integrity sha512-EsHVHVn7UgtxrhiM6tcBCXic56dTpl1WiKcIZhFyTFkA0vja7iZIJ3FYiYxT56g4hT0B9Lm7ZdBaPvEY3d1/eQ==
|
||||
|
||||
"@uniswap/lib@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-1.1.1.tgz#0afd29601846c16e5d082866cbb24a9e0758e6bc"
|
||||
@ -2568,10 +2587,10 @@
|
||||
tiny-warning "^1.0.3"
|
||||
toformat "^2.0.0"
|
||||
|
||||
"@uniswap/token-lists@^1.0.0-beta.11":
|
||||
version "1.0.0-beta.11"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.11.tgz#365dd55d536c67fa550554c0658391bfbc2930b7"
|
||||
integrity sha512-dGHdb58d+rN7G164ziPP6omb1R0hwBVgs95er83OzXKkVRlLKE/FLSdgpDaTxLj1war+P/hZXw2/ToYcKFsobQ==
|
||||
"@uniswap/token-lists@^1.0.0-beta.14":
|
||||
version "1.0.0-beta.14"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.14.tgz#599ae9eb04121736fd2e309a31227b0343d77511"
|
||||
integrity sha512-9+qzJqlQ6U4CE4RrMl1Gkh+ISWfnY/5bO7zFi+UGiJ2IEcZUYJm3bE2AcUc5g6WNAm7CL0uf1FU+fz3JlamOIg==
|
||||
|
||||
"@uniswap/v2-core@1.0.0":
|
||||
version "1.0.0"
|
||||
@ -4751,6 +4770,17 @@ ci-info@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
|
||||
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
|
||||
|
||||
cids@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cids/-/cids-1.0.0.tgz#5a148c4adbf3c56c45bbcf9000451907dfe8b5b2"
|
||||
integrity sha512-HEBCIElSiXlkgZq3dgHJc3eDcnFteFp96N8/1/oqX5lkxBtB66sZ12jqEP3g7Ut++jEk6kIUGifQ1Qrya1jcNQ==
|
||||
dependencies:
|
||||
class-is "^1.1.0"
|
||||
multibase "^3.0.0"
|
||||
multicodec "^2.0.0"
|
||||
multihashes "^3.0.1"
|
||||
uint8arrays "^1.0.0"
|
||||
|
||||
cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
|
||||
@ -4759,6 +4789,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
|
||||
inherits "^2.0.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
class-is@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/class-is/-/class-is-1.1.0.tgz#9d3c0fba0440d211d843cec3dedfa48055005825"
|
||||
integrity sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw==
|
||||
|
||||
class-utils@^0.3.5:
|
||||
version "0.3.6"
|
||||
resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
|
||||
@ -10247,6 +10282,14 @@ ms@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
multibase@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/multibase/-/multibase-3.0.0.tgz#f56eb828ee5c00241fe860ed5e2d144c8b9821f4"
|
||||
integrity sha512-fuB+zfRbF5zWV4L+CPM0dgA0gX7DHG/IMyzwhVi2RxbRVWn41Wk7SkKW8cxYDGOg6TVh7XgyoesjOAYrB1HBAA==
|
||||
dependencies:
|
||||
base-x "^3.0.8"
|
||||
web-encoding "^1.0.2"
|
||||
|
||||
multicast-dns-service-types@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
|
||||
@ -10260,6 +10303,23 @@ multicast-dns@^6.0.1:
|
||||
dns-packet "^1.3.1"
|
||||
thunky "^1.0.2"
|
||||
|
||||
multicodec@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/multicodec/-/multicodec-2.0.0.tgz#e0c41d99ce29d5f92a9406a6a31169a3802a1ca3"
|
||||
integrity sha512-2SLsdTCXqOpUfoSHkTaVzxnjjl5fsSO283Idb9rAYgKGVu188NFP5KncuZ8Ifg8H2gc5GOi2rkuhLumqv9nweQ==
|
||||
dependencies:
|
||||
uint8arrays "1.0.0"
|
||||
varint "^5.0.0"
|
||||
|
||||
multihashes@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-3.0.1.tgz#607c243d5e04ec022ac76c9c114e08416216f019"
|
||||
integrity sha512-fFY67WOtb0359IjDZxaCU3gJILlkwkFbxbwrK9Bej5+NqNaYztzLOj8/NgMNMg/InxmhK+Uu8S/U4EcqsHzB7Q==
|
||||
dependencies:
|
||||
multibase "^3.0.0"
|
||||
uint8arrays "^1.0.0"
|
||||
varint "^5.0.0"
|
||||
|
||||
mute-stream@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
||||
@ -12423,6 +12483,11 @@ react-use-gesture@^6.0.14:
|
||||
resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-6.0.14.tgz#ab2d35ef72a5fb6060a6160eb12568c276f8a4b1"
|
||||
integrity sha512-d9cnZJ0DOFd3FIO76J776DyhtbODgbxGKu19lvc1aSNTnRV5EKr9V4Uda188l2Qh0Va3pqWGxEQlw72r2cmnFQ==
|
||||
|
||||
react-virtualized-auto-sizer@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
|
||||
integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==
|
||||
|
||||
react-window@^1.8.5:
|
||||
version "1.8.5"
|
||||
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1"
|
||||
@ -14376,6 +14441,22 @@ ua-parser-js@^0.7.21:
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
|
||||
integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==
|
||||
|
||||
uint8arrays@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-1.0.0.tgz#9cf979517f85c32d6ef54adf824e3499bb715331"
|
||||
integrity sha512-14tqEVujDREW7YwonSZZwLvo7aFDfX7b6ubvM/U7XvZol+CC/LbhaX/550VlWmhddAL9Wou1sxp0Of3tGqXigg==
|
||||
dependencies:
|
||||
multibase "^3.0.0"
|
||||
web-encoding "^1.0.2"
|
||||
|
||||
uint8arrays@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-1.1.0.tgz#d034aa65399a9fd213a1579e323f0b29f67d0ed2"
|
||||
integrity sha512-cLdlZ6jnFczsKf5IH1gPHTtcHtPGho5r4CvctohmQjw8K7Q3gFdfIGHxSTdTaCKrL4w09SsPRJTqRS0drYeszA==
|
||||
dependencies:
|
||||
multibase "^3.0.0"
|
||||
web-encoding "^1.0.2"
|
||||
|
||||
unicode-canonical-property-names-ecmascript@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
|
||||
@ -14599,6 +14680,11 @@ value-equal@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
|
||||
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
|
||||
|
||||
varint@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.0.tgz#d826b89f7490732fabc0c0ed693ed475dcb29ebf"
|
||||
integrity sha1-2Ca4n3SQcy+rwMDtaT7Uddyynr8=
|
||||
|
||||
vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
@ -14705,6 +14791,11 @@ wbuf@^1.1.0, wbuf@^1.7.3:
|
||||
dependencies:
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
web-encoding@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/web-encoding/-/web-encoding-1.0.2.tgz#e5050d4826597f242eb6e0e5fede05e3b9512ca5"
|
||||
integrity sha512-fe9pqxglgy25Z4Ds+2GwZIrOnLxeozydMj0iV8zx0ZNxE3MPTseF4oXGrrBuH4vSkoDYDXoPRRFY/FEYitEUTA==
|
||||
|
||||
web3-provider-engine@15.0.12:
|
||||
version "15.0.12"
|
||||
resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-15.0.12.tgz#24d7f2f6fb6de856824c7306291018c4fc543ac3"
|
||||
|
Loading…
Reference in New Issue
Block a user