Compare commits

...

32 Commits

Author SHA1 Message Date
Moody Salem
4b57059353 fix(release): fix the dns record update 2020-05-27 09:58:28 -04:00
Moody Salem
6926f9a4ae fix(flatMap): don't use the native flatMap function, bump release plugin version 2020-05-27 09:29:53 -04:00
Moody Salem
7dec580944 chore(readme): prepare for more ipfs, use the latest version of github tag action 2020-05-27 09:14:12 -04:00
Moody Salem
5cf95680ef chore(release): fix cf-ipfs url 2020-05-27 09:01:20 -04:00
Moody Salem
f8d6bab4ae chore(release): limit release to release.yaml changes on v2 branch 2020-05-27 09:00:57 -04:00
Moody Salem
c9721c42bf perf(reduce call volume): save a bunch of calls to infura when the tab is not focused, change infura IDs for v2 2020-05-26 13:36:09 -04:00
Moody Salem
4414134bb2 chore(release): Fix release links 2020-05-26 13:17:53 -04:00
Moody Salem
44ba54e44a chore(release): add cf-ipfs.com 2020-05-26 13:03:50 -04:00
Moody Salem
9ec3109f72 chore(release): release text, convert cidv0 2020-05-26 13:01:38 -04:00
Moody Salem
e75793676a fix(release): include release changelog 2020-05-26 12:16:00 -04:00
Moody Salem
32006ded21 chore(release): trigger release on changes to release.yaml 2020-05-26 12:06:40 -04:00
Moody Salem
d4f1c579d8 chore(release): remove cancel action because it's confusing 2020-05-26 11:03:52 -04:00
Moody Salem
95f3541807 fix release.yaml script again 2020-05-26 10:30:12 -04:00
Moody Salem
da4ca73a1d post install scripts are needed for cypress 2020-05-26 10:07:32 -04:00
Moody Salem
e75bf8d003 typo in action name 2020-05-26 10:05:45 -04:00
Moody Salem
236f68a459 Missed a spot in the yarn tests 2020-05-26 09:59:59 -04:00
Moody Salem
9f07baaad2 Move the github action to its own repo 2020-05-26 09:54:33 -04:00
Moody Salem
c75464e1aa chore(release): only trigger release on tag 2020-05-26 09:43:48 -04:00
Moody Salem
bc80585bb4 chore(release): frozen lockfiles 2020-05-26 09:41:54 -04:00
Moody Salem
ad45b2b7bb chore(release): speed up install significantly 2020-05-26 09:38:43 -04:00
Moody Salem
63ac89e9f3 chore(release): clean up release.yaml 2020-05-26 09:28:47 -04:00
Moody Salem
1b6ae0d3db chore(release): trigger a release on tagged commits 2020-05-26 09:16:22 -04:00
Moody Salem
7d67819604 chore(release): Add an action to replace Vercel DNS records 2020-05-26 09:07:09 -04:00
Moody Salem
7b9b332c42 fix(popover): ios safari not showing the popover 2020-05-25 01:39:50 -04:00
Moody Salem
01feae978a add a discord link (#833) 2020-05-24 04:47:41 -04:00
Moody Salem
2452d51e14 fix(remove swap button): weirdness on firefox 2020-05-23 22:21:20 -04:00
Moody Salem
bbdc258083 fix(search modal): prevent overlapping tooltips, always render lists 2020-05-23 20:14:41 -04:00
Moody Salem
27b103e3f7 fix(search modal): restore style on sort button 2020-05-23 20:10:16 -04:00
Moody Salem
2a751b9892 Workaround for popover placement on elements that move, styling for small amounts of search results 2020-05-23 20:08:12 -04:00
Moody Salem
175e93fbba chore(release): Update the release text 2020-05-23 16:07:23 -04:00
Moody Salem
0b5fc07ee5 fix(popover): use the same offset 2020-05-23 12:17:39 -04:00
Moody Salem
a0d4710a11 perf(Search modal): performance improvements (#829)
* Search modal performance improvements

* Move more code out of the search modal

* Add the question helper to the search

* Fix a couple lint errors

* Fix the tests, duplicate import text

* Hide the token info on small screens, flex the list

* Fix token sorting, have a link that focuses and shows a tooltip for the search input

* Remove reach tooltip css

* Fix pair balances in the search modal

* Get the arrow working

* Only clear the input when they re-open

* Better way to exclude props

* More small performance tweaks
2020-05-23 12:06:16 -04:00
40 changed files with 997 additions and 779 deletions

2
.env
View File

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

View File

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

View File

@@ -1,36 +1,49 @@
name: Release
# every morning
on:
# every morning
schedule:
- cron: '0 12 * * *'
# releases are triggered on changes to this file
push:
branches:
- v2
paths:
- '.github/workflows/release.yaml'
jobs:
create-release:
name: Create Release
bump_version:
name: Bump Version
runs-on: ubuntu-latest
outputs:
new_tag: ${{ steps.github_tag_action.outputs.new_tag }}
changelog: ${{ steps.github_tag_action.outputs.changelog }}
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Bump version and push tag
id: bump_version
uses: mathieudutour/github-tag-action@v4
id: github_tag_action
uses: mathieudutour/github-tag-action@v4.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
release_branches: .*
- name: Cancel this build if no new commits
if: ${{ steps.bump_version.outputs.new_tag == null }}
uses: andymckay/cancel-action@0.2
create_release:
name: Create Release
runs-on: ubuntu-latest
needs: bump_version
if: ${{ needs.bump_version.outputs.new_tag != null }}
steps:
- name: Checkout
uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: '12'
- name: Install yarn
run: npm install -g yarn
- name: Install dependencies
run: yarn
run: yarn install --ignore-scripts --frozen-lockfile
- name: Build the IPFS bundle
run: yarn ipfs-build
@@ -39,14 +52,26 @@ jobs:
id: upload
uses: anantaramdas/ipfs-pinata-deploy-action@v1.5.2
with:
pin-name: Uniswap ${{ steps.bump_version.outputs.new_tag }}
pin-name: Uniswap ${{ needs.bump_version.outputs.new_tag }}
path: './build'
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
- name: Convert CIDv0 to CIDv1
id: convert_cidv0
uses: uniswap/convert-cidv0-cidv1@v1.0.0
with:
cidv0: ${{ steps.upload.outputs.hash }}
- name: Update DNS with new IPFS hash
id: update_dns
run: npx vercel --token ${{ secrets.VERCEL_TOKEN }} --scope uniswap dns add uniswap.org _dnslink.app TXT "dnslink=/ipfs/${{ steps.upload.outputs.hash }}"
uses: uniswap/replace-vercel-dns-records@v1.0.0
with:
domain: 'uniswap.org'
subdomain: '_dnslink.app'
record-type: 'TXT'
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
token: ${{ secrets.VERCEL_TOKEN }}
team-name: 'uniswap'
- name: Create GitHub Release
id: create_release
@@ -54,17 +79,27 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.bump_version.outputs.new_tag }}
release_name: Release ${{ steps.bump_version.outputs.new_tag }}
tag_name: ${{ needs.bump_version.outputs.new_tag }}
release_name: Release ${{ needs.bump_version.outputs.new_tag }}
body: |
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
The IPFS hash of the bundle is `${{ steps.upload.outputs.hash }}`
The IPFS hash of the bundle is:
- CIDv0: `${{ steps.upload.outputs.hash }}`
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
The following IPFS Gateways can be used to access the release:
Uniswap uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on Uniswap without your permission.
You can avoid this issue by using a subdomain IPFS gateway. The preferred gateway URLs below utilize the CIDv1 of the release in the subdomain, and are relatively safer.
Preferred URLs:
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.cf-ipfs.com/
- [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.upload.outputs.hash }}/)
Other IPFS gateways:
- https://cloudflare-ipfs.com/ipfs/${{ steps.upload.outputs.hash }}/
- https://ipfs.infura.io/ipfs/${{ steps.upload.outputs.hash }}/
- https://ipfs.io/ipfs/${{ steps.upload.outputs.hash }}/
- https://dweb.link/ipfs/${{ steps.upload.outputs.hash }}/
${{ steps.bump_version.outputs.changelog }}
${{ needs.bump_version.outputs.changelog }}

View File

@@ -26,7 +26,7 @@ jobs:
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn
- run: yarn install
- run: yarn integration-test
unit-tests:
@@ -48,7 +48,7 @@ jobs:
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn
- run: yarn install --ignore-scripts --frozen-lockfile
- run: yarn test
lint:
@@ -70,6 +70,6 @@ jobs:
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn
- run: yarn install --ignore-scripts --frozen-lockfile
- run: yarn lint

View File

@@ -1,9 +1,7 @@
# Uniswap Frontend
[![Netlify Status](https://api.netlify.com/api/v1/badges/fa110555-b3c7-4eeb-b840-88a835009c62/deploy-status)](https://app.netlify.com/sites/uniswap/deploys)
[![Tests](https://github.com/Uniswap/uniswap-frontend/workflows/Tests/badge.svg?branch=v2)](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
[![Tests](https://github.com/Uniswap/uniswap-frontend/workflows/Tests/badge.svg)](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
[![Styled With Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io/)
[![Release](https://github.com/Uniswap/uniswap-frontend/workflows/Release/badge.svg?branch=v2)](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ARelease)
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
@@ -17,10 +15,9 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
## Accessing the frontend
The front end is deployed to IPFS as well as to [uniswap.exchange](https://uniswap.exchange).
To access the front end via IPFS, use a link from the
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest).
To access the front end, use an IPFS gateway link from the
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest)
or visit [uniswap.exchange](https://uniswap.exchange).
## Development

View File

@@ -5,9 +5,9 @@ describe('Pool', () => {
cy.get('#token-search-input').type('DAI')
})
it.skip('can import a pool', () => {
it('can import a pool', () => {
cy.get('#join-pool-button').click()
cy.get('#import-pool-link').click() // blocked by the grid element in the search box
cy.get('#import-pool-link').click({ force: true }) // blocked by the grid element in the search box
cy.url().should('include', '/find')
})
})

View File

@@ -32,8 +32,10 @@ describe('Swap', () => {
it('can swap ETH for DAI', () => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click()
cy.get('#swap-currency-input .token-amount-input').type('0.001')
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')
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true })
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
cy.get('#show-advanced').click()
cy.get('#swap-button').click()

View File

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

View File

@@ -16,16 +16,18 @@
"@material-ui/core": "^4.9.5",
"@mycrypto/eth-scan": "^2.1.0",
"@popperjs/core": "^2.4.0",
"@reach/dialog": "^0.2.8",
"@reach/tooltip": "^0.2.0",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
"@reduxjs/toolkit": "^1.3.5",
"@types/jest": "^25.2.1",
"@types/lodash.flatmap": "^4.5.6",
"@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-window": "^1.8.2",
"@types/rebass": "^4.0.5",
"@types/styled-components": "^4.2.0",
"@types/testing-library__cypress": "^5.0.5",
@@ -54,6 +56,7 @@
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1",
"jazzicon": "^1.5.0",
"lodash.flatmap": "^4.5.0",
"polished": "^3.3.2",
"prettier": "^1.17.0",
"qrcode.react": "^0.9.3",
@@ -70,6 +73,7 @@
"react-scripts": "^3.4.1",
"react-spring": "^8.0.27",
"react-use-gesture": "^6.0.14",
"react-window": "^1.8.5",
"rebass": "^4.0.7",
"redux-localstorage-simple": "^2.2.0",
"serve": "^11.3.0",
@@ -107,4 +111,4 @@
]
},
"license": "GPL-3.0-or-later"
}
}

View File

@@ -1,11 +1,9 @@
import { Pair, Token } from '@uniswap/sdk'
import React, { useState, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import '@reach/tooltip/styles.css'
import { darken } from 'polished'
import { Field } from '../../state/swap/actions'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo'
import SearchModal from '../SearchModal'

View File

@@ -1,6 +1,6 @@
import React, { useRef, useEffect } 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 { Link } from '../../theme'
@@ -70,6 +70,10 @@ const MenuItem = styled(Link)`
:hover {
color: ${({ theme }) => theme.text1};
cursor: pointer;
text-decoration: none;
}
> svg {
margin-right: 8px;
}
`
@@ -102,26 +106,32 @@ export default function Menu() {
return (
<StyledMenu ref={node}>
<StyledMenuButton onClick={() => toggle()}>
<StyledMenuButton onClick={toggle}>
<StyledMenuIcon />
</StyledMenuButton>
{open ? (
{open && (
<MenuFlyout>
<MenuItem id="link" href="https://uniswap.org/">
<Info size={14} />
About
</MenuItem>
<MenuItem id="link" href="https://uniswap.org/docs/v2">
<BookOpen size={14} />
Docs
</MenuItem>
<MenuItem id="link" href={CODE_LINK}>
<Code size={14} />
Code
</MenuItem>
<MenuItem id="link" href="https://discord.gg/vXCdddD">
<MessageCircle size={14} />
Discord
</MenuItem>
<MenuItem id="link" href="https://uniswap.info/">
<PieChart size={14} />
Analytics
</MenuItem>
</MenuFlyout>
) : (
''
)}
</StyledMenu>
)

View File

@@ -11,7 +11,7 @@ import { useGesture } from 'react-use-gesture'
// errors emitted, fix with https://github.com/styled-components/styled-components/pull/3006
const AnimatedDialogOverlay = animated(DialogOverlay)
const StyledDialogOverlay = styled(AnimatedDialogOverlay)`
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ mobile: boolean }>`
&[data-reach-dialog-overlay] {
z-index: 2;
display: flex;
@@ -95,7 +95,7 @@ interface ModalProps {
onDismiss: () => void
minHeight?: number | false
maxHeight?: number
initialFocusRef?: React.Ref<any>
initialFocusRef?: React.RefObject<any>
children?: React.ReactNode
}
@@ -145,7 +145,7 @@ export default function Modal({
style={props}
onDismiss={onDismiss}
initialFocusRef={initialFocusRef}
mobile={isMobile}
mobile={true}
>
<Spring // animation for entrance and exit
from={{
@@ -191,15 +191,9 @@ export default function Modal({
style={props}
onDismiss={onDismiss}
initialFocusRef={initialFocusRef}
mobile={isMobile ? isMobile : undefined}
mobile={false}
>
<StyledDialogContent
hidden={true}
minHeight={minHeight}
maxHeight={maxHeight}
isOpen={isOpen}
mobile={isMobile ? isMobile : undefined}
>
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}>
<HiddenCloseButton onClick={onDismiss} />
{children}
</StyledDialogContent>

View File

@@ -7,7 +7,7 @@ import { withRouter, NavLink, Link as HistoryLink, RouteComponentProps } from 'r
import { CursorPointer } from '../../theme'
import { ArrowLeft } from 'react-feather'
import { RowBetween } from '../Row'
import QuestionHelper from '../Question'
import QuestionHelper from '../QuestionHelper'
import { useBodyKeyDown } from '../../hooks'

View File

@@ -0,0 +1,135 @@
import { Placement } from '@popperjs/core'
import { transparentize } from 'polished'
import React, { useState } from 'react'
import { usePopper } from 'react-popper'
import styled, { keyframes } from 'styled-components'
import useInterval from '../../hooks/useInterval'
import Portal from '@reach/portal'
const fadeIn = keyframes`
from {
opacity : 0;
}
to {
opacity : 1;
}
`
const fadeOut = keyframes`
from {
opacity : 1;
}
to {
opacity : 0;
}
`
const PopoverContainer = styled.div<{ show: boolean }>`
z-index: 9999;
visibility: ${props => (!props.show ? 'hidden' : 'visible')};
animation: ${props => (!props.show ? fadeOut : fadeIn)} 150ms linear;
transition: visibility 150ms linear;
background: ${({ theme }) => theme.bg2};
border: 1px solid ${({ theme }) => theme.bg3};
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)};
color: ${({ theme }) => theme.text2};
border-radius: 8px;
`
const ReferenceElement = styled.div`
display: inline-block;
`
const Arrow = styled.div`
width: 8px;
height: 8px;
z-index: 9998;
::before {
position: absolute;
width: 8px;
height: 8px;
z-index: 9998;
content: '';
border: 1px solid ${({ theme }) => theme.bg3};
transform: rotate(45deg);
background: ${({ theme }) => theme.bg2};
}
&.arrow-top {
bottom: -5px;
::before {
border-top: none;
border-left: none;
}
}
&.arrow-bottom {
top: -5px;
::before {
border-bottom: none;
border-right: none;
}
}
&.arrow-left {
right: -5px;
::before {
border-bottom: none;
border-left: none;
}
}
&.arrow-right {
left: -5px;
::before {
border-right: none;
border-top: none;
}
}
`
export interface PopoverProps {
content: React.ReactNode
show: boolean
children: React.ReactNode
placement?: Placement
}
export default function Popover({ content, show, children, placement = 'auto' }: PopoverProps) {
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>(null)
const [popperElement, setPopperElement] = useState<HTMLDivElement>(null)
const [arrowElement, setArrowElement] = useState<HTMLDivElement>(null)
const { styles, update, attributes } = usePopper(referenceElement, popperElement, {
placement,
strategy: 'fixed',
modifiers: [
{ name: 'offset', options: { offset: [8, 8] } },
{ name: 'arrow', options: { element: arrowElement } }
]
})
useInterval(update, show ? 100 : null)
return (
<>
<ReferenceElement ref={setReferenceElement}>{children}</ReferenceElement>
<Portal>
<PopoverContainer show={show} ref={setPopperElement} style={styles.popper} {...attributes.popper}>
{content}
<Arrow
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
ref={setArrowElement}
style={styles.arrow}
{...attributes.arrow}
/>
</PopoverContainer>
</Portal>
</>
)
}

View File

@@ -1,101 +0,0 @@
import React, { useState } from 'react'
import { createPortal } from 'react-dom'
import styled, { keyframes } from 'styled-components'
import { HelpCircle as Question } from 'react-feather'
import { usePopper } from 'react-popper'
const Wrapper = styled.div`
position: relative;
`
const QuestionWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
margin-left: 0.4rem;
padding: 0.2rem;
border: none;
background: none;
outline: none;
cursor: default;
border-radius: 36px;
background-color: ${({ theme }) => theme.bg2};
color: ${({ theme }) => theme.text2};
:hover,
:focus {
opacity: 0.7;
}
`
const fadeIn = keyframes`
from {
opacity : 0;
}
to {
opacity : 1;
}
`
const Popup = styled.div`
width: 228px;
z-index: 9999;
padding: 0.6rem 1rem;
line-height: 150%;
background: ${({ theme }) => theme.bg1};
border: 1px solid ${({ theme }) => theme.bg3};
border-radius: 8px;
animation: ${fadeIn} 0.15s linear;
color: ${({ theme }) => theme.text2};
font-weight: 400;
`
export default function QuestionHelper({ text }: { text: string }) {
const [showPopup, setShowPopup] = useState<boolean>(false)
const [referenceElement, setReferenceElement] = useState(null)
const [popperElement, setPopperElement] = useState(null)
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: 'auto',
strategy: 'fixed',
modifiers: [
{
name: 'offset',
options: {
offset: [6, 6]
}
}
]
})
const portal = createPortal(
showPopup && (
<Popup ref={setPopperElement} style={styles.popper} {...attributes.popper}>
{text}
</Popup>
),
document.getElementById('popover-container')
)
return (
<Wrapper>
<QuestionWrapper
onClick={() => {
setShowPopup(true)
}}
onMouseEnter={() => {
setShowPopup(true)
}}
onMouseLeave={() => {
setShowPopup(false)
}}
ref={setReferenceElement}
>
<Question size={16} />
</QuestionWrapper>
{portal}
</Wrapper>
)
}

View File

@@ -0,0 +1,40 @@
import React, { useCallback, useState } from 'react'
import { HelpCircle as Question } from 'react-feather'
import styled from 'styled-components'
import Tooltip from '../Tooltip'
const QuestionWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
padding: 0.2rem;
border: none;
background: none;
outline: none;
cursor: default;
border-radius: 36px;
background-color: ${({ theme }) => theme.bg2};
color: ${({ theme }) => theme.text2};
:hover,
:focus {
opacity: 0.7;
}
`
export default function QuestionHelper({ text, disabled }: { text: string; disabled?: boolean }) {
const [show, setShow] = useState<boolean>(false)
const open = useCallback(() => setShow(true), [setShow])
const close = useCallback(() => setShow(false), [setShow])
return (
<span style={{ marginLeft: 4 }}>
<Tooltip text={text} show={show && !disabled}>
<QuestionWrapper onClick={open} onMouseEnter={open} onMouseLeave={close}>
<Question size={16} />
</QuestionWrapper>
</Tooltip>
</span>
)
}

View File

@@ -0,0 +1,46 @@
import React from 'react'
import { Text } from 'rebass'
import { COMMON_BASES } from '../../constants'
import { AutoColumn } from '../Column'
import QuestionHelper from '../QuestionHelper'
import { AutoRow } from '../Row'
import TokenLogo from '../TokenLogo'
import { BaseWrapper } from './styleds'
export default function CommonBases({
chainId,
onSelect,
selectedTokenAddress
}: {
chainId: number
selectedTokenAddress: string
onSelect: (tokenAddress: string) => void
}) {
return (
<AutoColumn gap="md">
<AutoRow>
<Text fontWeight={500} fontSize={16}>
Common Bases
</Text>
<QuestionHelper text="These tokens are commonly used in pairs." />
</AutoRow>
<AutoRow gap="10px">
{COMMON_BASES[chainId]?.map(token => {
return (
<BaseWrapper
gap="6px"
onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)}
disable={selectedTokenAddress === token.address}
key={token.address}
>
<TokenLogo address={token.address} />
<Text fontWeight={500} fontSize={16}>
{token.symbol}
</Text>
</BaseWrapper>
)
})}
</AutoRow>
</AutoColumn>
)
}

View File

@@ -0,0 +1,64 @@
import { JSBI, Pair, TokenAmount } from '@uniswap/sdk'
import React from 'react'
import { FixedSizeList } from 'react-window'
import { Text } from 'rebass'
import { ButtonPrimary } from '../Button'
import DoubleTokenLogo from '../DoubleLogo'
import { RowFixed } from '../Row'
import { MenuItem, ModalInfo } from './styleds'
export default function PairList({
pairs,
focusTokenAddress,
pairBalances,
onSelectPair,
onAddLiquidity = onSelectPair
}: {
pairs: Pair[]
focusTokenAddress?: string
pairBalances: { [pairAddress: string]: TokenAmount }
onSelectPair: (pair: Pair) => void
onAddLiquidity: (pair: Pair) => void
}) {
if (pairs.length === 0) {
return <ModalInfo>No Pools Found</ModalInfo>
}
return (
<FixedSizeList
itemSize={54}
height={500}
itemCount={pairs.length}
width="100%"
style={{ flex: '1', minHeight: 200 }}
>
{({ index, style }) => {
const pair = pairs[index]
// the focused token is shown first
const tokenA = focusTokenAddress === pair.token1.address ? pair.token1 : pair.token0
const tokenB = tokenA === pair.token0 ? pair.token1 : pair.token0
const pairAddress = pair.liquidityToken.address
const balance = pairBalances[pairAddress]?.toSignificant(6)
const zeroBalance = pairBalances[pairAddress]?.raw && JSBI.equal(pairBalances[pairAddress].raw, JSBI.BigInt(0))
const selectPair = () => onSelectPair(pair)
const addLiquidity = () => onAddLiquidity(pair)
return (
<MenuItem style={style} onClick={selectPair}>
<RowFixed>
<DoubleTokenLogo a0={tokenA.address} a1={tokenB.address} size={24} margin={true} />
<Text fontWeight={500} fontSize={16}>{`${tokenA.symbol}/${tokenB.symbol}`}</Text>
</RowFixed>
<ButtonPrimary padding={'6px 8px'} width={'fit-content'} borderRadius={'12px'} onClick={addLiquidity}>
{balance ? (zeroBalance ? 'Join' : 'Add Liquidity') : 'Join'}
</ButtonPrimary>
</MenuItem>
)
}}
</FixedSizeList>
)
}

View File

@@ -0,0 +1,34 @@
import React from 'react'
import { Text } from 'rebass'
import styled from 'styled-components'
import { RowFixed } from '../Row'
export const FilterWrapper = styled(RowFixed)`
padding: 8px;
background-color: ${({ theme }) => theme.bg2};
color: ${({ theme }) => theme.text1};
border-radius: 8px;
user-select: none;
& > * {
user-select: none;
}
:hover {
cursor: pointer;
}
`
export default function SortButton({
toggleSortOrder,
ascending
}: {
toggleSortOrder: () => void
ascending: boolean
}) {
return (
<FilterWrapper onClick={toggleSortOrder}>
<Text fontSize={14} fontWeight={500}>
{ascending ? '↑' : '↓'}
</Text>
</FilterWrapper>
)
}

View File

@@ -0,0 +1,122 @@
import { ChainId, JSBI, Token, TokenAmount } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { FixedSizeList } from 'react-window'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import Circle from '../../assets/images/circle.svg'
import { ALL_TOKENS } from '../../constants/tokens'
import { useActiveWeb3React } from '../../hooks'
import { Link as StyledLink, TYPE } from '../../theme'
import { isAddress } from '../../utils'
import { ButtonSecondary } from '../Button'
import Column, { AutoColumn } from '../Column'
import { RowFixed } from '../Row'
import TokenLogo from '../TokenLogo'
import { FadedSpan, GreySpan, MenuItem, SpinnerWrapper, ModalInfo } from './styleds'
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
const address = isAddress(tokenAddress)
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
}
export default function TokenList({
tokens,
allTokenBalances,
selectedToken,
onTokenSelect,
otherToken,
showSendWithSwap,
onRemoveAddedToken,
otherSelectedText
}: {
tokens: Token[]
selectedToken: string
allTokenBalances: { [tokenAddress: string]: TokenAmount }
onTokenSelect: (tokenAddress: string) => void
onRemoveAddedToken: (chainId: number, tokenAddress: string) => void
otherToken: string
showSendWithSwap?: boolean
otherSelectedText: string
}) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
if (tokens.length === 0) {
return <ModalInfo>{t('noToken')}</ModalInfo>
}
return (
<FixedSizeList
width="100%"
height={500}
itemCount={tokens.length}
itemSize={50}
style={{ flex: '1', minHeight: 200 }}
>
{({ index, style }) => {
const { address, symbol } = tokens[index]
const customAdded = !isDefaultToken(address, chainId)
const balance = allTokenBalances[address]
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
return (
<MenuItem
style={style}
key={address}
className={`token-item-${address}`}
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
disabled={selectedToken && selectedToken === address}
selected={otherToken === address}
>
<RowFixed>
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>
{symbol}
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
</Text>
<FadedSpan>
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
{customAdded && (
<div
onClick={event => {
event.stopPropagation()
onRemoveAddedToken(chainId, address)
}}
>
<StyledLink style={{ marginLeft: '4px', fontWeight: 400 }}>(Remove)</StyledLink>
</div>
)}
</FadedSpan>
</Column>
</RowFixed>
<AutoColumn gap="4px" justify="end">
{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 ? (
<SpinnerWrapper src={Circle} alt="loader" />
) : (
'-'
)}
</AutoColumn>
</MenuItem>
)
}}
</FixedSizeList>
)
}

View File

@@ -1,24 +0,0 @@
import React from 'react'
import { Text } from 'rebass'
import { FilterWrapper } from './styleds'
export function TokenSortButton({
title,
toggleSortOrder,
invertSearchOrder
}: {
title: string
toggleSortOrder: () => void
invertSearchOrder: boolean
}) {
return (
<FilterWrapper onClick={toggleSortOrder}>
<Text fontSize={14} fontWeight={500}>
{title}
</Text>
<Text fontSize={14} fontWeight={500}>
{!invertSearchOrder ? '↓' : '↑'}
</Text>
</FilterWrapper>
)
}

View File

@@ -0,0 +1,50 @@
import { isAddress } from '../../utils'
import { Pair, Token } from '@uniswap/sdk'
export function filterTokens(tokens: Token[], search: string): Token[] {
if (search.length === 0) return tokens
const searchingAddress = isAddress(search)
if (searchingAddress) {
return tokens.filter(token => token.address === searchingAddress)
}
const lowerSearchParts = searchingAddress ? [] : search.toLowerCase().split(/\s+/)
const matchesSearch = (s: string): boolean => {
const sParts = s.toLowerCase().split(/\s+/)
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p)))
}
return tokens.filter(token => {
const { symbol, name } = token
return matchesSearch(symbol) || matchesSearch(name)
})
}
export function filterPairs(pairs: Pair[], search: string): Pair[] {
if (search.trim().length === 0) return pairs
const addressSearch = isAddress(search)
if (addressSearch) {
return pairs.filter(p => {
return (
p.token0.address === addressSearch ||
p.token1.address === addressSearch ||
p.liquidityToken.address === addressSearch
)
})
}
const lowerSearch = search.toLowerCase()
return pairs.filter(pair => {
const pairExpressionA = `${pair.token0.symbol}/${pair.token1.symbol}`.toLowerCase()
if (pairExpressionA.startsWith(lowerSearch)) return true
const pairExpressionB = `${pair.token1.symbol}/${pair.token0.symbol}`.toLowerCase()
if (pairExpressionB.startsWith(lowerSearch)) return true
return filterTokens([pair.token0, pair.token1], search).length > 0
})
}

View File

@@ -1,43 +1,29 @@
import '@reach/tooltip/styles.css'
import { ChainId, JSBI, Token, WETH } from '@uniswap/sdk'
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
import { Pair, Token } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { isMobile } from 'react-device-detect'
import { ArrowLeft } from 'react-feather'
import { useTranslation } from 'react-i18next'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import Circle from '../../assets/images/circle.svg'
import Card from '../../components/Card'
import { COMMON_BASES } from '../../constants'
import { ALL_TOKENS } from '../../constants/tokens'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalances } from '../../state/wallet/hooks'
import { CloseIcon, Link as StyledLink } from '../../theme/components'
import { escapeRegExp, isAddress } from '../../utils'
import { ButtonPrimary, ButtonSecondary } from '../Button'
import Column, { AutoColumn } from '../Column'
import DoubleTokenLogo from '../DoubleLogo'
import { isAddress } from '../../utils'
import Column from '../Column'
import Modal from '../Modal'
import QuestionHelper from '../Question'
import { AutoRow, RowBetween, RowFixed } from '../Row'
import TokenLogo from '../TokenLogo'
import { useTokenComparator } from './sorting'
import {
BaseWrapper,
FadedSpan,
GreySpan,
Input,
ItemList,
MenuItem,
PaddedColumn,
SpinnerWrapper,
TokenModalInfo
} from './styleds'
import { TokenSortButton } from './TokenSortButton'
import QuestionHelper from '../QuestionHelper'
import { AutoRow, RowBetween } from '../Row'
import Tooltip from '../Tooltip'
import CommonBases from './CommonBases'
import { filterPairs, filterTokens } from './filtering'
import PairList from './PairList'
import { balanceComparator, useTokenComparator } from './sorting'
import { PaddedColumn, SearchInput } from './styleds'
import TokenList from './TokenList'
import SortButton from './SortButton'
interface SearchModalProps extends RouteComponentProps {
isOpen?: boolean
@@ -51,11 +37,6 @@ interface SearchModalProps extends RouteComponentProps {
showCommonBases?: boolean
}
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
const address = isAddress(tokenAddress)
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
}
function SearchModal({
history,
isOpen,
@@ -72,421 +53,177 @@ function SearchModal({
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const isTokenView = filterType === 'tokens'
const allTokens = useAllTokens()
const allPairs = useAllDummyPairs()
const allBalances = useAllTokenBalancesTreatingWETHasETH()
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH()[account] ?? {}
const allPairBalances = useTokenBalances(
account,
allPairs.map(p => p.liquidityToken)
)
const [searchQuery, setSearchQuery] = useState('')
const [invertSearchOrder, setInvertSearchOrder] = useState(false)
const [searchQuery, setSearchQuery] = useState<string>('')
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
const removeTokenByAddress = useRemoveUserAddedToken()
// if the current input is an address, and we don't have the token in context, try to fetch it
const searchQueryToken = useTokenByAddressAndAutomaticallyAdd(searchQuery)
// toggle specific token import view
const [showTokenImport, setShowTokenImport] = useState(false)
// used to help scanning on results, put token found from input on left
const [identifiedToken, setIdentifiedToken] = useState<Token>()
// reset view on close
useEffect(() => {
if (!isOpen) {
setShowTokenImport(false)
}
}, [isOpen])
// if the current input is an address, and we don't have the token in context, try to fetch it and import
useTokenByAddressAndAutomaticallyAdd(searchQuery)
const tokenComparator = useTokenComparator(invertSearchOrder)
const sortedTokenList = useMemo(() => {
return Object.values(allTokens)
.sort(tokenComparator)
.map(token => {
return {
name: token.name,
symbol: token.symbol,
address: token.address,
balance: allBalances[account]?.[token.address]
}
})
}, [allTokens, tokenComparator, allBalances, account])
const sortedTokens: Token[] = useMemo(() => {
if (!isTokenView) return []
return Object.values(allTokens).sort(tokenComparator)
}, [allTokens, isTokenView, tokenComparator])
const filteredTokenList = useMemo(() => {
return sortedTokenList.filter(tokenEntry => {
const customAdded = !isDefaultToken(tokenEntry.address, chainId)
const filteredTokens: Token[] = useMemo(() => {
if (!isTokenView) return []
return filterTokens(sortedTokens, searchQuery)
}, [isTokenView, sortedTokens, searchQuery])
// if token import page dont show preset list, else show all
const include = !showTokenImport || (showTokenImport && customAdded && searchQuery !== '')
const inputIsAddress = searchQuery.slice(0, 2) === '0x'
const regexMatches = Object.keys(tokenEntry).map(tokenEntryKey => {
if (tokenEntryKey === 'address') {
return (
include &&
inputIsAddress &&
typeof tokenEntry[tokenEntryKey] === 'string' &&
!!tokenEntry[tokenEntryKey].match(new RegExp(escapeRegExp(searchQuery), 'i'))
)
}
return (
include &&
typeof tokenEntry[tokenEntryKey] === 'string' &&
!!tokenEntry[tokenEntryKey].match(new RegExp(escapeRegExp(searchQuery), 'i'))
)
})
return regexMatches.some(m => m)
})
}, [sortedTokenList, chainId, showTokenImport, searchQuery])
function _onTokenSelect(address) {
setSearchQuery('')
function _onTokenSelect(address: string) {
onTokenSelect(address)
onDismiss()
}
// clear the input on open
useEffect(() => {
if (isOpen) setSearchQuery('')
}, [isOpen, setSearchQuery])
// manage focus on modal show
const inputRef = useRef()
const inputRef = useRef<HTMLInputElement>()
function onInput(event) {
const input = event.target.value
const checksummedInput = isAddress(input)
setSearchQuery(checksummedInput || input)
}
function clearInputAndDismiss() {
setSearchQuery('')
onDismiss()
}
// make an effort to identify the specific token a user is searching for
useEffect(() => {
const searchQueryIsAddress = !!isAddress(searchQuery)
// try to find an exact match by address
if (searchQueryIsAddress) {
const identifiedTokenByAddress = Object.values(allTokens).filter(token => {
return searchQueryIsAddress && token.address === isAddress(searchQuery)
})
if (identifiedTokenByAddress.length > 0) setIdentifiedToken(identifiedTokenByAddress[0])
}
// try to find an exact match by symbol
else {
const identifiedTokenBySymbol = Object.values(allTokens).filter(token => {
return token.symbol.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()
})
if (identifiedTokenBySymbol.length > 0) setIdentifiedToken(identifiedTokenBySymbol[0])
}
return () => {
setIdentifiedToken(undefined)
}
}, [allTokens, searchQuery])
const sortedPairList = useMemo(() => {
if (isTokenView) return []
return allPairs.sort((a, b): number => {
// sort by balance
const balanceA = allBalances[account]?.[a.liquidityToken.address]
const balanceB = allBalances[account]?.[b.liquidityToken.address]
if (balanceA?.greaterThan('0') && !balanceB?.greaterThan('0')) return !invertSearchOrder ? -1 : 1
if (!balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) return !invertSearchOrder ? 1 : -1
if (balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) {
return balanceA.greaterThan(balanceB) ? (!invertSearchOrder ? -1 : 1) : !invertSearchOrder ? 1 : -1
}
return 0
const balanceA = allPairBalances[a.liquidityToken.address]
const balanceB = allPairBalances[b.liquidityToken.address]
return balanceComparator(balanceA, balanceB)
})
}, [allPairs, allBalances, account, invertSearchOrder])
}, [isTokenView, allPairs, allPairBalances])
const filteredPairList = useMemo(() => {
const searchQueryIsAddress = !!isAddress(searchQuery)
return sortedPairList.filter(pair => {
// if there's no search query, hide non-ETH pairs
if (searchQuery === '') return pair.token0.equals(WETH[chainId]) || pair.token1.equals(WETH[chainId])
const filteredPairs = useMemo(() => {
if (isTokenView) return []
return filterPairs(sortedPairList, searchQuery)
}, [isTokenView, searchQuery, sortedPairList])
const token0 = pair.token0
const token1 = pair.token1
const selectPair = useCallback(
(pair: Pair) => {
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
},
[history]
)
if (searchQueryIsAddress) {
if (token0.address === isAddress(searchQuery)) return true
if (token1.address === isAddress(searchQuery)) return true
} else {
const identifier0 = `${token0.symbol}/${token1.symbol}`
const identifier1 = `${token1.symbol}/${token0.symbol}`
if (identifier0.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()) return true
if (identifier1.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()) return true
}
return false
})
}, [searchQuery, sortedPairList, chainId])
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
})[0]
function renderPairsList() {
if (filteredPairList?.length === 0) {
return (
<PaddedColumn justify="center">
<Text>No Pools Found</Text>
</PaddedColumn>
)
}
return (
filteredPairList &&
filteredPairList.map((pair, i) => {
// reset ordering to help scan search results
const token0 = identifiedToken ? (identifiedToken.equals(pair.token0) ? pair.token0 : pair.token1) : pair.token0
const token1 = identifiedToken ? (identifiedToken.equals(pair.token0) ? pair.token1 : pair.token0) : pair.token1
const pairAddress = pair.liquidityToken.address
const balance = allBalances?.[account]?.[pairAddress]?.toSignificant(6)
const zeroBalance =
allBalances?.[account]?.[pairAddress]?.raw &&
JSBI.equal(allBalances?.[account]?.[pairAddress].raw, JSBI.BigInt(0))
return (
<MenuItem
key={i}
onClick={() => {
history.push('/add/' + token0.address + '-' + token1.address)
onDismiss()
}}
>
<RowFixed>
<DoubleTokenLogo a0={token0?.address || ''} a1={token1?.address || ''} size={24} margin={true} />
<Text fontWeight={500} fontSize={16}>{`${token0?.symbol}/${token1?.symbol}`}</Text>
</RowFixed>
<ButtonPrimary
padding={'6px 8px'}
width={'fit-content'}
borderRadius={'12px'}
onClick={() => {
history.push('/add/' + token0.address + '-' + token1.address)
onDismiss()
}}
>
{balance ? (zeroBalance ? 'Join' : 'Add Liquidity') : 'Join'}
</ButtonPrimary>
</MenuItem>
)
})
)
}
function renderTokenList() {
if (filteredTokenList.length === 0) {
if (isAddress(searchQuery)) {
if (!searchQueryToken) {
return <TokenModalInfo>Searching...</TokenModalInfo>
} else {
// a user found a token by search that isn't yet added to localstorage
return (
<MenuItem
key={searchQueryToken.address}
className={`temporary-token-${searchQueryToken.address}`}
onClick={() => {
_onTokenSelect(searchQueryToken.address)
}}
>
<RowFixed>
<TokenLogo address={searchQueryToken.address} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>{searchQueryToken.symbol}</Text>
<FadedSpan>(Found by search)</FadedSpan>
</Column>
</RowFixed>
</MenuItem>
)
}
} else {
return <TokenModalInfo>{t('noToken')}</TokenModalInfo>
}
} else {
return filteredTokenList.map(({ address, symbol, balance }) => {
const customAdded = !isDefaultToken(address, chainId)
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
// if token import page dont show preset list, else show all
return (
<MenuItem
key={address}
className={`token-item-${address}`}
onClick={() => (hiddenToken && hiddenToken === address ? null : _onTokenSelect(address))}
disabled={hiddenToken && hiddenToken === address}
selected={otherSelectedTokenAddress === address}
>
<RowFixed>
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>
{symbol}
{otherSelectedTokenAddress === address && <GreySpan> ({otherSelectedText})</GreySpan>}
</Text>
<FadedSpan>
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
{customAdded && (
<div
onClick={event => {
event.stopPropagation()
if (searchQuery === address) {
setSearchQuery('')
}
removeTokenByAddress(chainId, address)
}}
>
<StyledLink style={{ marginLeft: '4px', fontWeight: 400 }}>(Remove)</StyledLink>
</div>
)}
</FadedSpan>
</Column>
</RowFixed>
<AutoColumn gap="4px" justify="end">
{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 ? (
<SpinnerWrapper src={Circle} alt="loader" />
) : (
'-'
)}
</AutoColumn>
</MenuItem>
)
})
}
}
const openTooltip = useCallback(() => {
setTooltipOpen(true)
inputRef.current?.focus()
}, [setTooltipOpen])
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
return (
<Modal
isOpen={isOpen}
onDismiss={clearInputAndDismiss}
maxHeight={70}
initialFocusRef={isMobile ? undefined : inputRef}
>
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} initialFocusRef={isMobile ? undefined : inputRef}>
<Column style={{ width: '100%' }}>
{showTokenImport ? (
<PaddedColumn gap="lg">
<RowBetween>
<RowFixed>
<CursorPointer>
<ArrowLeft
onClick={() => {
setShowTokenImport(false)
}}
/>
</CursorPointer>
<Text fontWeight={500} fontSize={16} marginLeft={'10px'}>
Import A Token
</Text>
</RowFixed>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<TYPE.body style={{ marginTop: '10px' }}>
To import a custom token, paste token address in the search bar.
</TYPE.body>
<Input type={'text'} placeholder={'0x000000...'} value={searchQuery} ref={inputRef} onChange={onInput} />
{renderTokenList()}
</PaddedColumn>
) : (
<PaddedColumn gap="20px">
<RowBetween>
<Text fontWeight={500} fontSize={16}>
{filterType === 'tokens' ? 'Select a token' : 'Select a pool'}
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<Input
<PaddedColumn gap="20px">
<RowBetween>
<Text fontWeight={500} fontSize={16}>
{isTokenView ? 'Select a token' : 'Select a pool'}
<QuestionHelper
disabled={tooltipOpen}
text={
isTokenView
? 'Find a token by searching for its name or symbol or by pasting its address below.'
: 'Find a pair by searching for its name 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={onInput}
onBlur={closeTooltip}
/>
{showCommonBases && (
<AutoColumn gap="md">
<AutoRow>
<Text fontWeight={500} fontSize={16}>
Common Bases
</Text>
<QuestionHelper text="These tokens are commonly used in pairs." />
</AutoRow>
<AutoRow gap="10px">
{COMMON_BASES[chainId]?.map(token => {
return (
<BaseWrapper
gap="6px"
onClick={() => hiddenToken !== token.address && _onTokenSelect(token.address)}
disable={hiddenToken === token.address}
key={token.address}
>
<TokenLogo address={token.address} />
<Text fontWeight={500} fontSize={16}>
{token.symbol}
</Text>
</BaseWrapper>
)
})}
</AutoRow>
</AutoColumn>
</Tooltip>
{showCommonBases && (
<CommonBases chainId={chainId} onSelect={_onTokenSelect} selectedTokenAddress={hiddenToken} />
)}
<RowBetween>
<Text fontSize={14} fontWeight={500}>
{isTokenView ? 'Token Name' : 'Pool Name'}
</Text>
{isTokenView && (
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
)}
<RowBetween>
<Text fontSize={14} fontWeight={500}>
{filterType === 'tokens' ? 'Token Name' : 'Pool Name'}
</Text>
{filterType === 'tokens' && (
<TokenSortButton
invertSearchOrder={invertSearchOrder}
toggleSortOrder={() => setInvertSearchOrder(iso => !iso)}
title={filterType === 'tokens' ? 'Your Balances' : ' '}
/>
</RowBetween>
</PaddedColumn>
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
{isTokenView ? (
<TokenList
tokens={filteredTokens}
allTokenBalances={allTokenBalances}
onRemoveAddedToken={removeTokenByAddress}
onTokenSelect={_onTokenSelect}
otherSelectedText={otherSelectedText}
otherToken={otherSelectedTokenAddress}
selectedToken={hiddenToken}
showSendWithSwap={showSendWithSwap}
/>
) : (
<PairList
pairs={filteredPairs}
focusTokenAddress={focusedToken?.address}
onAddLiquidity={selectPair}
onSelectPair={selectPair}
pairBalances={allPairBalances}
/>
)}
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
<Card>
<AutoRow justify={'center'}>
<div>
{isTokenView ? (
<Text fontWeight={500} color={theme.text2} fontSize={14}>
<StyledLink onClick={openTooltip}>Having trouble finding a token?</StyledLink>
</Text>
) : (
<Text fontWeight={500}>
{!isMobile && "Don't see a pool? "}
<StyledLink
onClick={() => {
history.push('/find')
}}
>
{!isMobile ? 'Import it.' : 'Import pool.'}
</StyledLink>
</Text>
)}
</RowBetween>
</PaddedColumn>
)}
{!showTokenImport && <div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />}
{!showTokenImport && <ItemList>{filterType === 'tokens' ? renderTokenList() : renderPairsList()}</ItemList>}
{!showTokenImport && <div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />}
{!showTokenImport && (
<Card>
<AutoRow justify={'center'}>
<div>
{filterType !== 'tokens' && (
<Text fontWeight={500}>
{!isMobile && "Don't see a pool? "}
<StyledLink
onClick={() => {
history.push('/find')
}}
>
{!isMobile ? 'Import it.' : 'Import pool.'}
</StyledLink>
</Text>
)}
{filterType === 'tokens' && (
<Text fontWeight={500} color={theme.text2} fontSize={14}>
{!isMobile && "Don't see a token? "}
<StyledLink
onClick={() => {
setShowTokenImport(true)
}}
>
{!isMobile ? 'Import it.' : 'Import custom token.'}
</StyledLink>
</Text>
)}
</div>
</AutoRow>
</Card>
)}
</div>
</AutoRow>
</Card>
</Column>
</Modal>
)

View File

@@ -3,10 +3,21 @@ import { useMemo } from 'react'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
// compare two token amounts with highest one coming first
export function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
if (balanceA && balanceB) {
return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1
} else if (balanceA && balanceA.greaterThan('0')) {
return -1
} else if (balanceB && balanceB.greaterThan('0')) {
return 1
}
return 0
}
function getTokenComparator(
weth: Token | undefined,
balances: { [tokenAddress: string]: TokenAmount },
invertSearchOrder: boolean
balances: { [tokenAddress: string]: TokenAmount }
): (tokenA: Token, tokenB: Token) => number {
return function sortTokens(tokenA: Token, tokenB: Token): number {
// -1 = a is first
@@ -22,11 +33,8 @@ function getTokenComparator(
const balanceA = balances[tokenA.address]
const balanceB = balances[tokenB.address]
if (balanceA?.greaterThan('0') && !balanceB?.greaterThan('0')) return !invertSearchOrder ? -1 : 1
if (!balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) return !invertSearchOrder ? 1 : -1
if (balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) {
return balanceA.greaterThan(balanceB) ? (!invertSearchOrder ? -1 : 1) : !invertSearchOrder ? 1 : -1
}
const balanceComp = balanceComparator(balanceA, balanceB)
if (balanceComp !== 0) return balanceComp
// sort by symbol
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
@@ -37,5 +45,12 @@ export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: T
const { account, chainId } = useActiveWeb3React()
const weth = WETH[chainId]
const balances = useAllTokenBalancesTreatingWETHasETH()
return useMemo(() => getTokenComparator(weth, balances[account] ?? {}, inverted), [account, balances, inverted, weth])
const comparator = useMemo(() => getTokenComparator(weth, balances[account] ?? {}), [account, balances, weth])
return useMemo(() => {
if (inverted) {
return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1
} else {
return comparator
}
}, [inverted, comparator])
}

View File

@@ -3,7 +3,7 @@ import { Spinner } from '../../theme'
import { AutoColumn } from '../Column'
import { AutoRow, RowBetween, RowFixed } from '../Row'
export const TokenModalInfo = styled.div`
export const ModalInfo = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
padding: 1rem 1rem;
@@ -13,13 +13,6 @@ export const TokenModalInfo = styled.div`
min-height: 200px;
`
export const ItemList = styled.div`
flex-grow: 1;
height: 254px;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
`
export const FadedSpan = styled(RowFixed)`
color: ${({ theme }) => theme.primary1};
font-size: 14px;
@@ -59,20 +52,6 @@ export const Input = styled.input`
}
`
export const FilterWrapper = styled(RowFixed)`
padding: 8px;
background-color: ${({ selected, theme }) => selected && theme.bg2};
color: ${({ selected, theme }) => (selected ? theme.text1 : theme.text2)};
border-radius: 8px;
user-select: none;
& > * {
user-select: none;
}
:hover {
cursor: pointer;
}
`
export const PaddedColumn = styled(AutoColumn)`
padding: 20px;
padding-bottom: 12px;
@@ -106,3 +85,11 @@ export const BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
background-color: ${({ theme, disable }) => disable && theme.bg3};
opacity: ${({ disable }) => disable && '0.4'};
`
export const SearchInput = styled(Input)`
transition: border 100ms;
:focus {
border: 1px solid ${({ theme }) => theme.primary1};
outline: none;
}
`

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef, useCallback, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import QuestionHelper from '../Question'
import QuestionHelper from '../QuestionHelper'
import { Text } from 'rebass'
import { TYPE } from '../../theme'
import { AutoColumn } from '../Column'

View File

@@ -11,7 +11,7 @@ import { useTokenWarningDismissal } from '../../state/user/hooks'
import { Link, TYPE } from '../../theme'
import { getEtherscanLink } from '../../utils'
import PropsOfExcluding from '../../utils/props-of-excluding'
import QuestionHelper from '../Question'
import QuestionHelper from '../QuestionHelper'
import TokenLogo from '../TokenLogo'
const Wrapper = styled.div<{ error: boolean }>`

View File

@@ -0,0 +1,18 @@
import React from 'react'
import styled from 'styled-components'
import Popover, { PopoverProps } from '../Popover'
const TooltipContainer = styled.div`
width: 228px;
padding: 0.6rem 1rem;
line-height: 150%;
font-weight: 400;
`
interface TooltipProps extends Omit<PopoverProps, 'content'> {
text: string
}
export default function Tooltip({ text, ...rest }: TooltipProps) {
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
}

View File

@@ -8,11 +8,12 @@ import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
import { AutoColumn } from '../Column'
import { SectionBreak } from './styleds'
import QuestionHelper from '../Question'
import QuestionHelper from '../QuestionHelper'
import { RowBetween, RowFixed } from '../Row'
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
import FormattedPriceImpact from './FormattedPriceImpact'
import TokenLogo from '../TokenLogo'
import flatMap from 'lodash.flatmap'
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
trade: Trade
@@ -109,27 +110,28 @@ export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: A
justifyContent="space-evenly"
alignItems="center"
>
{trade.route.path
{flatMap(
trade.route.path,
// add a null in-between each item
.flatMap((token, i, array) => {
(token, i, array) => {
const lastItem = i === array.length - 1
return lastItem ? [token] : [token, null]
})
.map((token, i) => {
// use null as an indicator to insert chevrons
if (token === null) {
return <ChevronRight key={i} color={theme.text2} />
} else {
return (
<Flex my="0.5rem" alignItems="center" key={token.address} style={{ flexShrink: 0 }}>
<TokenLogo address={token.address} size="1.5rem" />
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
{token.symbol}
</TYPE.black>
</Flex>
)
}
})}
}
).map((token, i) => {
// use null as an indicator to insert chevrons
if (token === null) {
return <ChevronRight key={i} color={theme.text2} />
} else {
return (
<Flex my="0.5rem" alignItems="center" key={token.address} style={{ flexShrink: 0 }}>
<TokenLogo address={token.address} size="1.5rem" />
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
{token.symbol}
</TYPE.black>
</Flex>
)
}
})}
</Flex>
</AutoColumn>
)}

View File

@@ -8,7 +8,7 @@ import { TYPE } from '../../theme'
import { formatExecutionPrice } from '../../utils/prices'
import { ButtonError } from '../Button'
import { AutoColumn } from '../Column'
import QuestionHelper from '../Question'
import QuestionHelper from '../QuestionHelper'
import { AutoRow, RowBetween, RowFixed } from '../Row'
import FormattedPriceImpact from './FormattedPriceImpact'
import { StyledBalanceMaxMini } from './styleds'

View File

@@ -16,6 +16,7 @@ export default function useInterval(callback: () => void, delay: null | number)
}
if (delay !== null) {
tick()
const id = setInterval(tick, delay)
return () => clearInterval(id)
}

View File

@@ -0,0 +1,20 @@
import { useCallback, useEffect, useState } from 'react'
/**
* Returns whether the window is currently visible to the user.
*/
export default function useIsWindowVisible(): boolean {
const [focused, setFocused] = useState<boolean>(true)
const listener = useCallback(() => {
setFocused(document.visibilityState !== 'hidden')
}, [setFocused])
useEffect(() => {
document.addEventListener('visibilitychange', listener)
return () => {
document.removeEventListener('visibilitychange', listener)
}
}, [listener])
return focused
}

View File

@@ -3,7 +3,7 @@ import styled, { ThemeContext } from 'styled-components'
import { JSBI, Pair } from '@uniswap/sdk'
import { RouteComponentProps } from 'react-router-dom'
import Question from '../../components/Question'
import Question from '../../components/QuestionHelper'
import SearchModal from '../../components/SearchModal'
import PositionCard from '../../components/PositionCard'
import { useTokenBalances } from '../../state/wallet/hooks'

View File

@@ -11,7 +11,7 @@ import Card, { BlueCard, GreyCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import QuestionHelper from '../../components/Question'
import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
@@ -370,7 +370,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
</ArrowWrapper>
<ButtonSecondary
onClick={() => setSendingWithSwap(false)}
style={{ marginRight: '0px', width: 'fit-content', fontSize: '14px' }}
style={{ marginRight: '0px', width: 'auto', fontSize: '14px' }}
padding={'4px 6px'}
>
Remove Swap

View File

@@ -10,7 +10,7 @@ import Card, { GreyCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import QuestionHelper from '../../components/Question'
import QuestionHelper from '../../components/QuestionHelper'
import { RowBetween, RowFixed } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { useDebounce, useActiveWeb3React } from '../../hooks'
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
import { updateBlockNumber } from './actions'
import { useDispatch } from 'react-redux'
@@ -7,6 +8,7 @@ export default function Updater() {
const { library, chainId } = useActiveWeb3React()
const dispatch = useDispatch()
const windowVisible = useIsWindowVisible()
const [maxBlockNumber, setMaxBlockNumber] = useState<number | null>(null)
// because blocks arrive in bunches with longer polling periods, we just want
// to process the latest one.
@@ -38,8 +40,10 @@ export default function Updater() {
useEffect(() => {
if (!chainId || !debouncedMaxBlockNumber) return
dispatch(updateBlockNumber({ chainId, blockNumber: debouncedMaxBlockNumber }))
}, [chainId, debouncedMaxBlockNumber, dispatch])
if (windowVisible) {
dispatch(updateBlockNumber({ chainId, blockNumber: debouncedMaxBlockNumber }))
}
}, [chainId, debouncedMaxBlockNumber, windowVisible, dispatch])
return null
}

View File

@@ -3,7 +3,7 @@ import { useActiveWeb3React } from '../../hooks'
import { useCallback, useMemo } from 'react'
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
import { useAllTokens } from '../../hooks/Tokens'
import { getTokenDecimals, getTokenName, getTokenSymbol, isAddress } from '../../utils'
import { getTokenInfoWithFallback, isAddress } from '../../utils'
import { AppDispatch, AppState } from '../index'
import {
addSerializedPair,
@@ -14,6 +14,7 @@ import {
SerializedToken,
updateUserDarkMode
} from './actions'
import flatMap from 'lodash.flatmap'
function serializeToken(token: Token): SerializedToken {
return {
@@ -69,11 +70,7 @@ export function useFetchTokenByAddress(): (address: string) => Promise<Token | n
if (!library || !chainId) return null
const validatedAddress = isAddress(address)
if (!validatedAddress) return null
const [decimals, symbol, name] = await Promise.all([
getTokenDecimals(address, library).catch(() => null),
getTokenSymbol(address, library).catch(() => 'UNKNOWN'),
getTokenName(address, library).catch(() => 'Unknown')
])
const { name, symbol, decimals } = await getTokenInfoWithFallback(validatedAddress, library)
if (decimals === null) {
return null
@@ -169,10 +166,11 @@ export function useAllDummyPairs(): Pair[] {
const generatedPairs: Pair[] = useMemo(
() =>
Object.values(tokens)
// select only tokens on the current chain
.filter(token => token.chainId === chainId)
.flatMap(token => {
flatMap(
Object.values(tokens)
// select only tokens on the current chain
.filter(token => token.chainId === chainId),
token => {
// for each token on the current chain,
return (
bases
@@ -188,7 +186,8 @@ export function useAllDummyPairs(): Pair[] {
})
.filter(pair => !!pair) as Pair[]
)
}),
}
),
[tokens, chainId]
)

View File

@@ -60,15 +60,11 @@ const StyledLink = styled.a`
export function Link({
onClick,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
as,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ref,
target = '_blank',
href,
rel = 'noopener noreferrer',
...rest
}: HTMLProps<HTMLAnchorElement>) {
}: Omit<HTMLProps<HTMLAnchorElement>, 'as' | 'ref'>) {
const handleClick = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
onClick && onClick(event) // first call back into the original onClick

View File

@@ -102,42 +102,49 @@ export function getExchangeContract(pairAddress: string, library: Web3Provider,
return getContract(pairAddress, IUniswapV2PairABI, library, account)
}
// get token name
export async function getTokenName(tokenAddress: string, library: Web3Provider) {
// get token info and fall back to unknown if not available, except for the
// decimals which falls back to null
export async function getTokenInfoWithFallback(
tokenAddress: string,
library: Web3Provider
): Promise<{ name: string; symbol: string; decimals: null | number }> {
if (!isAddress(tokenAddress)) {
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
}
return getContract(tokenAddress, ERC20_ABI, library)
.name()
.catch(() =>
getContract(tokenAddress, ERC20_BYTES32_ABI, library)
.name()
.then(parseBytes32String)
)
}
const token = getContract(tokenAddress, ERC20_ABI, library)
// get token symbol
export async function getTokenSymbol(tokenAddress: string, library: Web3Provider) {
if (!isAddress(tokenAddress)) {
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
}
const namePromise: Promise<string> = token.name().catch(() =>
getContract(tokenAddress, ERC20_BYTES32_ABI, library)
.name()
.then(parseBytes32String)
.catch((e: Error) => {
console.debug('Failed to get name for token address', e, tokenAddress)
return 'Unknown'
})
)
return getContract(tokenAddress, ERC20_ABI, library)
.symbol()
.catch(() => {
const contractBytes32 = getContract(tokenAddress, ERC20_BYTES32_ABI, library)
return contractBytes32.symbol().then(parseBytes32String)
})
}
const symbolPromise: Promise<string> = token.symbol().catch(() => {
const contractBytes32 = getContract(tokenAddress, ERC20_BYTES32_ABI, library)
return contractBytes32
.symbol()
.then(parseBytes32String)
.catch((e: Error) => {
console.debug('Failed to get symbol for token address', e, tokenAddress)
return 'UNKNOWN'
})
})
const decimalsPromise: Promise<number | null> = token.decimals().catch((e: Error) => {
console.debug('Failed to get decimals for token address', e, tokenAddress)
return null
})
// get token decimals
export async function getTokenDecimals(tokenAddress: string, library: Web3Provider) {
if (!isAddress(tokenAddress)) {
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
}
return getContract(tokenAddress, ERC20_ABI, library).decimals()
const [name, symbol, decimals]: [string, string, number | null] = (await Promise.all([
namePromise,
symbolPromise,
decimalsPromise
])) as [string, string, number | null]
return { name, symbol, decimals }
}
export function escapeRegExp(string: string): string {

191
yarn.lock
View File

@@ -2385,68 +2385,34 @@
penpal "3.0.7"
pocket-js-core "0.0.3"
"@reach/auto-id@0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.2.0.tgz#97f9e48fe736aa5c6f4f32cf73c1f19d005f8550"
integrity sha512-lVK/svL2HuQdp7jgvlrLkFsUx50Az9chAhxpiPwBqcS83I2pVWvXp98FOcSCCJCV++l115QmzHhFd+ycw1zLBg==
"@reach/component-component@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@reach/component-component/-/component-component-0.1.3.tgz#5d156319572dc38995b246f81878bc2577c517e5"
integrity sha512-a1USH7L3bEfDdPN4iNZGvMEFuBfkdG+QNybeyDv8RloVFgZYRoM+KGXyy2KOfEnTUM8QWDRSROwaL3+ts5Angg==
"@reach/dialog@^0.2.8":
version "0.2.9"
resolved "https://registry.yarnpkg.com/@reach/dialog/-/dialog-0.2.9.tgz#6375ec4adc1e22838aeede15f57d5eb5ac0e571c"
integrity sha512-4plyTRt2X4bB9A5fDFXH0bxb4aipLyAuRVMpOuA1RekFgvkxFn65em2CWYSzRsVf3aTQ2cEnYET4a5H2Qu8a5Q==
"@reach/dialog@^0.10.3":
version "0.10.3"
resolved "https://registry.yarnpkg.com/@reach/dialog/-/dialog-0.10.3.tgz#ba789809c3b194fff79d3bcb4a583c58e03edb83"
integrity sha512-RMpUHNjRQhkjGzKt9/oLmDhwUBikW3JbEzgzZngq5MGY5kWRPwYInLDkEA8We4E43AbBsl5J/PRzQha9V+EEXw==
dependencies:
"@reach/component-component" "^0.1.3"
"@reach/portal" "^0.2.1"
"@reach/utils" "^0.2.3"
react-focus-lock "^1.17.7"
react-remove-scroll "^1.0.2"
"@reach/observe-rect@^1.0.3":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.1.0.tgz#4e967a93852b6004c3895d9ed8d4e5b41895afde"
integrity sha512-kE+jvoj/OyJV24C03VvLt5zclb9ArJi04wWXMMFwQvdZjdHoBlN4g0ZQFjyy/ejPF1Z/dpUD5dhRdBiUmIGZTA==
"@reach/portal@^0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.2.1.tgz#07720b999e0063a9e179c14dbdc60fd991cfc9fa"
integrity sha512-pUQ0EtCcYm4ormEjJmdk4uzZCxOpaRHB8FDKJXy6q6GqRqQwZ4lAT1f2Tvw0DAmULmyZTpe1/heXY27Tdnct+Q==
dependencies:
"@reach/component-component" "^0.1.3"
"@reach/rect@^0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.2.1.tgz#7343020174c90e2290b844d17c03fd9c78e6b601"
integrity sha512-aZ9RsNHDMQ3zETonikqu9/85iXxj+LPqZ9Gr9UAncj3AufYmGeWG3XG6b37B+7ORH+mkhVpLU2ZlIWxmOe9Cqg==
dependencies:
"@reach/component-component" "^0.1.3"
"@reach/observe-rect" "^1.0.3"
"@reach/tooltip@^0.2.0":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@reach/tooltip/-/tooltip-0.2.2.tgz#a861ce38269b586597ab40417323b33d3d6dc927"
integrity sha512-afcfqH6EzDHmwTB6g1k0dSbkyT0s9KPIi5bX56nNuldsCIasImFFYDjRZLhFcuxjskwIsHAi06yC3GV6mtcRxw==
dependencies:
"@reach/auto-id" "0.2.0"
"@reach/portal" "^0.2.1"
"@reach/rect" "^0.2.1"
"@reach/utils" "^0.2.3"
"@reach/visually-hidden" "^0.1.4"
"@reach/portal" "^0.10.3"
"@reach/utils" "^0.10.3"
prop-types "^15.7.2"
react-focus-lock "^2.3.1"
react-remove-scroll "^2.3.0"
tslib "^1.11.2"
"@reach/utils@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.2.3.tgz#820f6a6af4301b4c5065cfc04bb89e6a3d1d723f"
integrity sha512-zM9rA8jDchr05giMhL95dPeYkK67cBQnIhCVrOKKqgWGsv+2GE/HZqeptvU4zqs0BvIqsThwov+YxVNVh5csTQ==
"@reach/portal@^0.10.3":
version "0.10.3"
resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.10.3.tgz#2eb408cc246d3eabbbf3b47ca4dc9c381cdb1d88"
integrity sha512-t8c+jtDxMLSPRGg93sQd2s6dDNilh5/qdrwmx88ki7l9h8oIXqMxPP3kSkOqZ9cbVR0b2A68PfMhCDOwMGvkoQ==
dependencies:
"@reach/utils" "^0.10.3"
tslib "^1.11.2"
"@reach/visually-hidden@^0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.1.4.tgz#0dc4ecedf523004337214187db70a46183bd945b"
integrity sha512-QHbzXjflSlCvDd6vJwdwx16mSB+vUCCQMiU/wK/CgVNPibtpEiIbisyxkpZc55DyDFNUIqP91rSUsNae+ogGDQ==
"@reach/utils@^0.10.3":
version "0.10.3"
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.10.3.tgz#e30f9b172d131161953df7dd01553c57ca4e78f8"
integrity sha512-LoIZSfVAJMA+DnzAMCMfc/wAM39iKT8BQQ9gI1FODpxd8nPFP4cKisMuRXImh2/iVtG2Z6NzzCNgceJSrywqFQ==
dependencies:
"@types/warning" "^3.0.0"
tslib "^1.11.2"
warning "^4.0.3"
"@reduxjs/toolkit@^1.3.5":
version "1.3.5"
@@ -2852,6 +2818,18 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
"@types/lodash.flatmap@^4.5.6":
version "4.5.6"
resolved "https://registry.yarnpkg.com/@types/lodash.flatmap/-/lodash.flatmap-4.5.6.tgz#5f1ea80cebe403f0fbfcc1b5ad75cd09dd8b5785"
integrity sha512-ELNrUL9q+MB7AACaHivWIsKDFDgYhHE3/svXhqvDJgONtn2c467Cy87nEb7CEDvfaGCPv91lPaW596I8s5oiNQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.153"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.153.tgz#5cb7dded0649f1df97938ac5ffc4f134e9e9df98"
integrity sha512-lYniGRiRfZf2gGAR9cfRC3Pi5+Q1ziJCKqPmjZocigrSJUVPWf7st1BtSJ8JOeK0FLXVndQ1IjUjTco9CXGo/Q==
"@types/lodash@4.14.149":
version "4.14.149"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440"
@@ -2950,6 +2928,13 @@
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"
integrity sha512-gP1xam68Wc4ZTAee++zx6pTdDAH08rAkQrWm4B4F/y6hhmlT9Mgx2q8lTCXnrPHXsr15XjRN9+K2DLKcz44qEQ==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.9.34":
version "16.9.34"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.34.tgz#f7d5e331c468f53affed17a8a4d488cd44ea9349"
@@ -3052,6 +3037,11 @@
dependencies:
pretty-format "^25.1.0"
"@types/warning@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52"
integrity sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=
"@types/web3-provider-engine@^14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/@types/web3-provider-engine/-/web3-provider-engine-14.0.0.tgz#43adc3b39dc9812b82aef8cd2d66577665ad59b0"
@@ -8644,7 +8634,7 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
inherits "^2.0.3"
readable-stream "^2.3.6"
focus-lock@^0.6.3:
focus-lock@^0.6.7:
version "0.6.8"
resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.6.8.tgz#61985fadfa92f02f2ee1d90bc738efaf7f3c9f46"
integrity sha512-vkHTluRCoq9FcsrldC0ulQHiyBYgVJB2CX53I8r0nTC6KnEij7Of0jpBspjt3/CuNb6fyoj3aOh9J2HgQUM0og==
@@ -8919,6 +8909,11 @@ get-caller-file@^2.0.1:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-nonce@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
get-own-enumerable-property-symbols@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
@@ -11887,7 +11882,7 @@ memdown@~3.0.0:
ltgt "~2.2.0"
safe-buffer "~5.1.1"
memoize-one@^5.0.0:
"memoize-one@>=3.1.1 <6", memoize-one@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
@@ -14430,7 +14425,7 @@ react-app-polyfill@^1.0.6:
regenerator-runtime "^0.13.3"
whatwg-fetch "^3.0.0"
react-clientside-effect@^1.2.0:
react-clientside-effect@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837"
integrity sha512-nRmoyxeok5PBO6ytPvSjKp9xwXg9xagoTK1mMjwnQxqM9Hd7MNPl+LS1bOSOe+CV2+4fnEquc7H/S8QD3q697A==
@@ -14501,15 +14496,17 @@ react-feather@^2.0.8:
dependencies:
prop-types "^15.7.2"
react-focus-lock@^1.17.7:
version "1.19.1"
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-1.19.1.tgz#2f3429793edaefe2d077121f973ce5a3c7a0651a"
integrity sha512-TPpfiack1/nF4uttySfpxPk4rGZTLXlaZl7ncZg/ELAk24Iq2B1UUaUioID8H8dneUXqznT83JTNDHDj+kwryw==
react-focus-lock@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.3.1.tgz#9d5d85899773609c7eefa4fc54fff6a0f5f2fc47"
integrity sha512-j15cWLPzH0gOmRrUg01C09Peu8qbcdVqr6Bjyfxj80cNZmH+idk/bNBYEDSmkAtwkXI+xEYWSmHYqtaQhZ8iUQ==
dependencies:
"@babel/runtime" "^7.0.0"
focus-lock "^0.6.3"
focus-lock "^0.6.7"
prop-types "^15.6.2"
react-clientside-effect "^1.2.0"
react-clientside-effect "^1.2.2"
use-callback-ref "^1.2.1"
use-sidecar "^1.0.1"
react-ga@^2.5.7:
version "2.7.0"
@@ -14548,21 +14545,24 @@ react-redux@^7.2.0:
prop-types "^15.7.2"
react-is "^16.9.0"
react-remove-scroll-bar@^1.1.5:
version "1.2.0"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-1.2.0.tgz#07250b2bc581f56315759c454c9b159dd04ba49d"
integrity sha512-8xSYR6xgW8QW65k38qB1Sh6ouTRjZ7BEteepR9tACd1rSaRyVYWabFxYLNOr4l1blZlqb81GEmDpUoPm7LsUTA==
react-remove-scroll-bar@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.1.0.tgz#edafe9b42a42c0dad9bdd10712772a1f9a39d7b9"
integrity sha512-5X5Y5YIPjIPrAoMJxf6Pfa7RLNGCgwZ95TdnVPgPuMftRfO8DaC7F4KP1b5eiO8hHbe7u+wZNDbYN5WUTpv7+g==
dependencies:
react-style-singleton "^1.1.0"
react-style-singleton "^2.1.0"
tslib "^1.0.0"
react-remove-scroll@^1.0.2:
version "1.0.8"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-1.0.8.tgz#a5aadc56368345a51ba524582c842a773849f609"
integrity sha512-AS6gFBO6T2CP0TgmDjq3Ip0Fz1HKyv+lzNrQAkJBSWGyOYaMWLMDy77mQJ7qEyy6fK0pI+Cz5x3X81/ux6SBew==
react-remove-scroll@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.3.0.tgz#3af06fe2f7130500704b676cdef94452c08fe593"
integrity sha512-UqVimLeAe+5EHXKfsca081hAkzg3WuDmoT9cayjBegd6UZVhlTEchleNp9J4TMGkb/ftLve7ARB5Wph+HJ7A5g==
dependencies:
react-remove-scroll-bar "^1.1.5"
react-remove-scroll-bar "^2.1.0"
react-style-singleton "^2.1.0"
tslib "^1.0.0"
use-callback-ref "^1.2.3"
use-sidecar "^1.0.1"
react-router-dom@^5.0.0:
version "5.1.2"
@@ -14661,11 +14661,12 @@ react-spring@^8.0.27:
"@babel/runtime" "^7.3.1"
prop-types "^15.5.8"
react-style-singleton@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-1.1.1.tgz#b2b698765519da812b80f55ab3c5fc5d849a2e63"
integrity sha512-0JD+XC5veR3oxf7GzIXipr89sM8R3rWnOR/gpzIV0DnoRBrcTvvkqyMu9icDYqM/6CWJhYcH5Jdy6Nim7PmoTQ==
react-style-singleton@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.1.0.tgz#7396885332e9729957f9df51f08cadbfc164e1c4"
integrity sha512-DH4ED+YABC1dhvSDYGGreAHmfuTXj6+ezT3CmHoqIEfxNgEYfIMoOtmbRp42JsUst3IPqBTDL+8r4TF7EWhIHw==
dependencies:
get-nonce "^1.0.0"
invariant "^2.2.4"
tslib "^1.0.0"
@@ -14684,6 +14685,14 @@ 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-window@^1.8.5:
version "1.8.5"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1"
integrity sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==
dependencies:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
@@ -16893,6 +16902,11 @@ tslib@^1.0.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.2.tgz#9c79d83272c9a7aaf166f73915c9667ecdde3cc9"
integrity sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==
tslib@^1.11.2, tslib@^1.9.3:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
tsutils@^3.17.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
@@ -17235,11 +17249,24 @@ usb@^1.6.0:
nan "2.13.2"
prebuild-install "^5.3.3"
use-callback-ref@^1.2.1, use-callback-ref@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.3.tgz#9f939dfb5740807bbf9dd79cdd4e99d27e827756"
integrity sha512-DPBPh1i2adCZoIArRlTuKRy7yue7QogtEnfv0AKrWsY+GA+4EKe37zhRDouNnyWMoNQFYZZRF+2dLHsWE4YvJA==
use-media@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/use-media/-/use-media-1.4.0.tgz#e777bf1f382a7aacabbd1f9ce3da2b62e58b2a98"
integrity sha512-XsgyUAf3nhzZmEfhc5MqLHwyaPjs78bgytpVJ/xDl0TF4Bptf3vEpBNBBT/EIKOmsOc8UbuECq3mrP3mt1QANA==
use-sidecar@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.2.tgz#e72f582a75842f7de4ef8becd6235a4720ad8af6"
integrity sha512-287RZny6m5KNMTb/Kq9gmjafi7lQL0YHO1lYolU6+tY1h9+Z3uCtkJJ3OSOq3INwYf2hBryCcDh4520AhJibMA==
dependencies:
detect-node "^2.0.4"
tslib "^1.9.3"
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
@@ -17470,7 +17497,7 @@ walletlink@^2.0.2:
preact "^10.3.3"
rxjs "^6.5.4"
warning@^4.0.2:
warning@^4.0.2, warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==