Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01feae978a | ||
|
|
2452d51e14 | ||
|
|
bbdc258083 | ||
|
|
27b103e3f7 | ||
|
|
2a751b9892 | ||
|
|
175e93fbba | ||
|
|
0b5fc07ee5 | ||
|
|
a0d4710a11 | ||
|
|
63af1a160d | ||
|
|
85d52b3480 | ||
|
|
219de1f471 | ||
|
|
f110fa7732 |
20
.github/workflows/release.yaml
vendored
20
.github/workflows/release.yaml
vendored
@@ -44,6 +44,10 @@ jobs:
|
|||||||
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
|
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
|
||||||
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
|
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
|
||||||
|
|
||||||
|
- 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 }}"
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1.1.0
|
uses: actions/create-release@v1.1.0
|
||||||
@@ -52,16 +56,24 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.bump_version.outputs.new_tag }}
|
tag_name: ${{ steps.bump_version.outputs.new_tag }}
|
||||||
release_name: Release ${{ steps.bump_version.outputs.new_tag }}
|
release_name: Release ${{ steps.bump_version.outputs.new_tag }}
|
||||||
draft: false
|
|
||||||
body: |
|
body: |
|
||||||
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
|
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 `${{ steps.upload.outputs.hash }}`
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
Preferred URLs:
|
||||||
|
- [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.upload.outputs.hash }}/)
|
||||||
|
- https://dweb.link/ipfs/${{ steps.upload.outputs.hash }}/
|
||||||
|
|
||||||
|
Other IPFS gateways:
|
||||||
- https://cloudflare-ipfs.com/ipfs/${{ steps.upload.outputs.hash }}/
|
- https://cloudflare-ipfs.com/ipfs/${{ steps.upload.outputs.hash }}/
|
||||||
- https://ipfs.infura.io/ipfs/${{ steps.upload.outputs.hash }}/
|
- https://ipfs.infura.io/ipfs/${{ steps.upload.outputs.hash }}/
|
||||||
- https://ipfs.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 }}
|
${{ steps.bump_version.outputs.changelog }}
|
||||||
@@ -5,9 +5,9 @@ describe('Pool', () => {
|
|||||||
cy.get('#token-search-input').type('DAI')
|
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('#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')
|
cy.url().should('include', '/find')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ describe('Swap', () => {
|
|||||||
|
|
||||||
it('can swap ETH for DAI', () => {
|
it('can swap ETH for DAI', () => {
|
||||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click()
|
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
|
||||||
cy.get('#swap-currency-input .token-amount-input').type('0.001')
|
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('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||||
cy.get('#show-advanced').click()
|
cy.get('#show-advanced').click()
|
||||||
cy.get('#swap-button').click()
|
cy.get('#swap-button').click()
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
"@mycrypto/eth-scan": "^2.1.0",
|
"@mycrypto/eth-scan": "^2.1.0",
|
||||||
"@popperjs/core": "^2.4.0",
|
"@popperjs/core": "^2.4.0",
|
||||||
"@reach/dialog": "^0.2.8",
|
"@reach/dialog": "^0.2.8",
|
||||||
"@reach/tooltip": "^0.2.0",
|
|
||||||
"@reduxjs/toolkit": "^1.3.5",
|
"@reduxjs/toolkit": "^1.3.5",
|
||||||
"@types/jest": "^25.2.1",
|
"@types/jest": "^25.2.1",
|
||||||
"@types/node": "^13.13.5",
|
"@types/node": "^13.13.5",
|
||||||
@@ -26,6 +25,7 @@
|
|||||||
"@types/react-dom": "^16.9.7",
|
"@types/react-dom": "^16.9.7",
|
||||||
"@types/react-redux": "^7.1.8",
|
"@types/react-redux": "^7.1.8",
|
||||||
"@types/react-router-dom": "^5.0.0",
|
"@types/react-router-dom": "^5.0.0",
|
||||||
|
"@types/react-window": "^1.8.2",
|
||||||
"@types/rebass": "^4.0.5",
|
"@types/rebass": "^4.0.5",
|
||||||
"@types/styled-components": "^4.2.0",
|
"@types/styled-components": "^4.2.0",
|
||||||
"@types/testing-library__cypress": "^5.0.5",
|
"@types/testing-library__cypress": "^5.0.5",
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
"react-scripts": "^3.4.1",
|
"react-scripts": "^3.4.1",
|
||||||
"react-spring": "^8.0.27",
|
"react-spring": "^8.0.27",
|
||||||
"react-use-gesture": "^6.0.14",
|
"react-use-gesture": "^6.0.14",
|
||||||
|
"react-window": "^1.8.5",
|
||||||
"rebass": "^4.0.7",
|
"rebass": "^4.0.7",
|
||||||
"redux-localstorage-simple": "^2.2.0",
|
"redux-localstorage-simple": "^2.2.0",
|
||||||
"serve": "^11.3.0",
|
"serve": "^11.3.0",
|
||||||
@@ -107,4 +108,4 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0-or-later"
|
"license": "GPL-3.0-or-later"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Pair, Token } from '@uniswap/sdk'
|
import { Pair, Token } from '@uniswap/sdk'
|
||||||
import React, { useState, useContext } from 'react'
|
import React, { useState, useContext } from 'react'
|
||||||
import styled, { ThemeContext } from 'styled-components'
|
import styled, { ThemeContext } from 'styled-components'
|
||||||
import '@reach/tooltip/styles.css'
|
|
||||||
import { darken } from 'polished'
|
import { darken } from 'polished'
|
||||||
import { Field } from '../../state/swap/actions'
|
import { Field } from '../../state/swap/actions'
|
||||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||||
|
|
||||||
import TokenLogo from '../TokenLogo'
|
import TokenLogo from '../TokenLogo'
|
||||||
import DoubleLogo from '../DoubleLogo'
|
import DoubleLogo from '../DoubleLogo'
|
||||||
import SearchModal from '../SearchModal'
|
import SearchModal from '../SearchModal'
|
||||||
@@ -125,7 +123,6 @@ interface CurrencyInputPanelProps {
|
|||||||
onMax?: () => void
|
onMax?: () => void
|
||||||
showMaxButton: boolean
|
showMaxButton: boolean
|
||||||
label?: string
|
label?: string
|
||||||
urlAddedTokens?: Token[]
|
|
||||||
onTokenSelection?: (tokenAddress: string) => void
|
onTokenSelection?: (tokenAddress: string) => void
|
||||||
token?: Token | null
|
token?: Token | null
|
||||||
disableTokenSelect?: boolean
|
disableTokenSelect?: boolean
|
||||||
@@ -145,7 +142,6 @@ export default function CurrencyInputPanel({
|
|||||||
onMax,
|
onMax,
|
||||||
showMaxButton,
|
showMaxButton,
|
||||||
label = 'Input',
|
label = 'Input',
|
||||||
urlAddedTokens = [], // used
|
|
||||||
onTokenSelection = null,
|
onTokenSelection = null,
|
||||||
token = null,
|
token = null,
|
||||||
disableTokenSelect = false,
|
disableTokenSelect = false,
|
||||||
@@ -246,7 +242,6 @@ export default function CurrencyInputPanel({
|
|||||||
setModalOpen(false)
|
setModalOpen(false)
|
||||||
}}
|
}}
|
||||||
filterType="tokens"
|
filterType="tokens"
|
||||||
urlAddedTokens={urlAddedTokens}
|
|
||||||
onTokenSelect={onTokenSelection}
|
onTokenSelect={onTokenSelection}
|
||||||
showSendWithSwap={showSendWithSwap}
|
showSendWithSwap={showSendWithSwap}
|
||||||
hiddenToken={token?.address}
|
hiddenToken={token?.address}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useRef, useEffect } from 'react'
|
import React, { useRef, useEffect } from 'react'
|
||||||
|
import { Info, BookOpen, Code, PieChart, MessageCircle } from 'react-feather'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
|
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
|
||||||
|
|
||||||
import { Link } from '../../theme'
|
import { Link } from '../../theme'
|
||||||
@@ -70,6 +70,10 @@ const MenuItem = styled(Link)`
|
|||||||
:hover {
|
:hover {
|
||||||
color: ${({ theme }) => theme.text1};
|
color: ${({ theme }) => theme.text1};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
> svg {
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -102,26 +106,32 @@ export default function Menu() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledMenu ref={node}>
|
<StyledMenu ref={node}>
|
||||||
<StyledMenuButton onClick={() => toggle()}>
|
<StyledMenuButton onClick={toggle}>
|
||||||
<StyledMenuIcon />
|
<StyledMenuIcon />
|
||||||
</StyledMenuButton>
|
</StyledMenuButton>
|
||||||
{open ? (
|
{open && (
|
||||||
<MenuFlyout>
|
<MenuFlyout>
|
||||||
<MenuItem id="link" href="https://uniswap.org/">
|
<MenuItem id="link" href="https://uniswap.org/">
|
||||||
|
<Info size={14} />
|
||||||
About
|
About
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem id="link" href="https://uniswap.org/docs/v2">
|
<MenuItem id="link" href="https://uniswap.org/docs/v2">
|
||||||
|
<BookOpen size={14} />
|
||||||
Docs
|
Docs
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem id="link" href={CODE_LINK}>
|
<MenuItem id="link" href={CODE_LINK}>
|
||||||
|
<Code size={14} />
|
||||||
Code
|
Code
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem id="link" href="https://discord.gg/vXCdddD">
|
||||||
|
<MessageCircle size={14} />
|
||||||
|
Discord
|
||||||
|
</MenuItem>
|
||||||
<MenuItem id="link" href="https://uniswap.info/">
|
<MenuItem id="link" href="https://uniswap.info/">
|
||||||
|
<PieChart size={14} />
|
||||||
Analytics
|
Analytics
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
)}
|
||||||
</StyledMenu>
|
</StyledMenu>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { withRouter, NavLink, Link as HistoryLink, RouteComponentProps } from 'r
|
|||||||
import { CursorPointer } from '../../theme'
|
import { CursorPointer } from '../../theme'
|
||||||
import { ArrowLeft } from 'react-feather'
|
import { ArrowLeft } from 'react-feather'
|
||||||
import { RowBetween } from '../Row'
|
import { RowBetween } from '../Row'
|
||||||
import QuestionHelper from '../Question'
|
import QuestionHelper from '../QuestionHelper'
|
||||||
|
|
||||||
import { useBodyKeyDown } from '../../hooks'
|
import { useBodyKeyDown } from '../../hooks'
|
||||||
|
|
||||||
|
|||||||
139
src/components/Popover/index.tsx
Normal file
139
src/components/Popover/index.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { transparentize } from 'polished'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { usePopper } from 'react-popper'
|
||||||
|
import styled, { keyframes } from 'styled-components'
|
||||||
|
import useInterval from '../../hooks/useInterval'
|
||||||
|
|
||||||
|
const fadeIn = keyframes`
|
||||||
|
from {
|
||||||
|
opacity : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity : 1;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fadeOut = keyframes`
|
||||||
|
from {
|
||||||
|
opacity : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity : 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||||
|
position: relative;
|
||||||
|
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`
|
||||||
|
position: absolute;
|
||||||
|
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
|
||||||
|
showPopup: boolean
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Popover({ content, showPopup, children }: 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: 'auto',
|
||||||
|
strategy: 'fixed',
|
||||||
|
modifiers: [
|
||||||
|
{ name: 'offset', options: { offset: [8, 8] } },
|
||||||
|
{ name: 'arrow', options: { element: arrowElement } }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const portal = createPortal(
|
||||||
|
<PopoverContainer show={showPopup} ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||||
|
{content}
|
||||||
|
<Arrow
|
||||||
|
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
|
||||||
|
ref={setArrowElement}
|
||||||
|
style={styles.arrow}
|
||||||
|
{...attributes.arrow}
|
||||||
|
/>
|
||||||
|
</PopoverContainer>,
|
||||||
|
document.getElementById('popover-container')
|
||||||
|
)
|
||||||
|
|
||||||
|
useInterval(update, showPopup ? 100 : null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ReferenceElement ref={setReferenceElement}>{children}</ReferenceElement>
|
||||||
|
{portal}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
47
src/components/QuestionHelper/index.tsx
Normal file
47
src/components/QuestionHelper/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import React, { 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 [showPopup, setShowPopup] = useState<boolean>(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span style={{ marginLeft: 4 }}>
|
||||||
|
<Tooltip text={text} showPopup={showPopup && !disabled}>
|
||||||
|
<QuestionWrapper
|
||||||
|
onClick={() => {
|
||||||
|
setShowPopup(true)
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setShowPopup(true)
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setShowPopup(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Question size={16} />
|
||||||
|
</QuestionWrapper>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
46
src/components/SearchModal/CommonBases.tsx
Normal file
46
src/components/SearchModal/CommonBases.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
64
src/components/SearchModal/PairList.tsx
Normal file
64
src/components/SearchModal/PairList.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
39
src/components/SearchModal/SortButton.tsx
Normal file
39
src/components/SearchModal/SortButton.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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({
|
||||||
|
title,
|
||||||
|
toggleSortOrder,
|
||||||
|
ascending
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
toggleSortOrder: () => void
|
||||||
|
ascending: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FilterWrapper onClick={toggleSortOrder}>
|
||||||
|
<Text fontSize={14} fontWeight={500}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize={14} fontWeight={500}>
|
||||||
|
{ascending ? '↑' : '↓'}
|
||||||
|
</Text>
|
||||||
|
</FilterWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
122
src/components/SearchModal/TokenList.tsx
Normal file
122
src/components/SearchModal/TokenList.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
50
src/components/SearchModal/filtering.ts
Normal file
50
src/components/SearchModal/filtering.ts
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,159 +1,37 @@
|
|||||||
import React, { useState, useRef, useMemo, useEffect, useContext } from 'react'
|
import { Pair, Token } from '@uniswap/sdk'
|
||||||
import '@reach/tooltip/styles.css'
|
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import styled, { ThemeContext } from 'styled-components'
|
|
||||||
import { JSBI, Token, WETH } from '@uniswap/sdk'
|
|
||||||
import { isMobile } from 'react-device-detect'
|
import { isMobile } from 'react-device-detect'
|
||||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
|
||||||
import { COMMON_BASES } from '../../constants'
|
|
||||||
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
|
||||||
import { Link as StyledLink } from '../../theme/components'
|
|
||||||
|
|
||||||
import Card from '../../components/Card'
|
|
||||||
import Modal from '../Modal'
|
|
||||||
import Circle from '../../assets/images/circle.svg'
|
|
||||||
import TokenLogo from '../TokenLogo'
|
|
||||||
import DoubleTokenLogo from '../DoubleLogo'
|
|
||||||
import Column, { AutoColumn } from '../Column'
|
|
||||||
import { Text } from 'rebass'
|
|
||||||
import { CursorPointer } from '../../theme'
|
|
||||||
import { ArrowLeft } from 'react-feather'
|
|
||||||
import { CloseIcon } from '../../theme/components'
|
|
||||||
import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
|
|
||||||
import { Spinner, TYPE } from '../../theme'
|
|
||||||
import { RowBetween, RowFixed, AutoRow } from '../Row'
|
|
||||||
|
|
||||||
import { isAddress, escapeRegExp } from '../../utils'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
|
||||||
import {
|
|
||||||
useAllDummyPairs,
|
|
||||||
useFetchTokenByAddress,
|
|
||||||
useAddUserToken,
|
|
||||||
useRemoveUserAddedToken,
|
|
||||||
useUserAddedTokens
|
|
||||||
} from '../../state/user/hooks'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useToken, useAllTokens } from '../../hooks/Tokens'
|
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||||
import QuestionHelper from '../Question'
|
import { Text } from 'rebass'
|
||||||
|
import { ThemeContext } from 'styled-components'
|
||||||
|
import Card from '../../components/Card'
|
||||||
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
|
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||||
|
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||||
|
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalances } from '../../state/wallet/hooks'
|
||||||
|
import { CloseIcon, Link as StyledLink } from '../../theme/components'
|
||||||
|
import { isAddress } from '../../utils'
|
||||||
|
import Column from '../Column'
|
||||||
|
import Modal from '../Modal'
|
||||||
|
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'
|
||||||
|
|
||||||
const TokenModalInfo = styled.div`
|
interface SearchModalProps extends RouteComponentProps {
|
||||||
${({ theme }) => theme.flexRowNoWrap}
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin: 0.25rem 0.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
user-select: none;
|
|
||||||
min-height: 200px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ItemList = styled.div`
|
|
||||||
flex-grow: 1;
|
|
||||||
height: 254px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
`
|
|
||||||
|
|
||||||
const FadedSpan = styled(RowFixed)`
|
|
||||||
color: ${({ theme }) => theme.primary1};
|
|
||||||
font-size: 14px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const GreySpan = styled.span`
|
|
||||||
color: ${({ theme }) => theme.text3};
|
|
||||||
font-weight: 400;
|
|
||||||
`
|
|
||||||
|
|
||||||
const SpinnerWrapper = styled(Spinner)`
|
|
||||||
margin: 0 0.25rem 0 0.25rem;
|
|
||||||
color: ${({ theme }) => theme.text4};
|
|
||||||
opacity: 0.6;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Input = styled.input`
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
padding: 16px;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
border-radius: 20px;
|
|
||||||
color: ${({ theme }) => theme.text1};
|
|
||||||
border-style: solid;
|
|
||||||
border: 1px solid ${({ theme }) => theme.bg3};
|
|
||||||
-webkit-appearance: none;
|
|
||||||
|
|
||||||
font-size: 18px;
|
|
||||||
|
|
||||||
::placeholder {
|
|
||||||
color: ${({ theme }) => theme.text3};
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const PaddedColumn = styled(AutoColumn)`
|
|
||||||
padding: 20px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const PaddedItem = styled(RowBetween)`
|
|
||||||
padding: 4px 20px;
|
|
||||||
height: 56px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const MenuItem = styled(PaddedItem)`
|
|
||||||
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)};
|
|
||||||
`
|
|
||||||
|
|
||||||
const BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
|
|
||||||
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
|
|
||||||
padding: 0 6px;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 120px;
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
cursor: ${({ disable }) => !disable && 'pointer'};
|
|
||||||
background-color: ${({ theme, disable }) => !disable && theme.bg2};
|
|
||||||
}
|
|
||||||
|
|
||||||
background-color: ${({ theme, disable }) => disable && theme.bg3};
|
|
||||||
opacity: ${({ disable }) => disable && '0.4'};
|
|
||||||
`
|
|
||||||
|
|
||||||
// filters on results
|
|
||||||
const FILTERS = {
|
|
||||||
VOLUME: 'VOLUME',
|
|
||||||
LIQUIDITY: 'LIQUIDITY',
|
|
||||||
BALANCES: 'BALANCES'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchModalProps extends RouteComponentProps<{}> {
|
|
||||||
isOpen?: boolean
|
isOpen?: boolean
|
||||||
onDismiss?: () => void
|
onDismiss?: () => void
|
||||||
filterType?: 'tokens'
|
filterType?: 'tokens'
|
||||||
hiddenToken?: string
|
hiddenToken?: string
|
||||||
showSendWithSwap?: boolean
|
showSendWithSwap?: boolean
|
||||||
onTokenSelect?: (address: string) => void
|
onTokenSelect?: (address: string) => void
|
||||||
urlAddedTokens?: Token[]
|
|
||||||
otherSelectedTokenAddress?: string
|
otherSelectedTokenAddress?: string
|
||||||
otherSelectedText?: string
|
otherSelectedText?: string
|
||||||
showCommonBases?: boolean
|
showCommonBases?: boolean
|
||||||
@@ -164,7 +42,6 @@ function SearchModal({
|
|||||||
isOpen,
|
isOpen,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
onTokenSelect,
|
onTokenSelect,
|
||||||
urlAddedTokens,
|
|
||||||
filterType,
|
filterType,
|
||||||
hiddenToken,
|
hiddenToken,
|
||||||
showSendWithSwap,
|
showSendWithSwap,
|
||||||
@@ -176,495 +53,180 @@ function SearchModal({
|
|||||||
const { account, chainId } = useActiveWeb3React()
|
const { account, chainId } = useActiveWeb3React()
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
|
const isTokenView = filterType === 'tokens'
|
||||||
|
|
||||||
const allTokens = useAllTokens()
|
const allTokens = useAllTokens()
|
||||||
const allPairs = useAllDummyPairs()
|
const allPairs = useAllDummyPairs()
|
||||||
const allBalances = useAllTokenBalancesTreatingWETHasETH()
|
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH()[account] ?? {}
|
||||||
|
const allPairBalances = useTokenBalances(
|
||||||
|
account,
|
||||||
|
allPairs.map(p => p.liquidityToken)
|
||||||
|
)
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||||
const [invertSearchOrder, setInvertSearchOrder] = useState(false)
|
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||||
|
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||||
|
|
||||||
const userAddedTokens = useUserAddedTokens()
|
|
||||||
const fetchTokenByAddress = useFetchTokenByAddress()
|
|
||||||
const addToken = useAddUserToken()
|
|
||||||
const removeTokenByAddress = useRemoveUserAddedToken()
|
const removeTokenByAddress = useRemoveUserAddedToken()
|
||||||
|
|
||||||
// if the current input is an address, and we don't have the token in context, try to fetch it
|
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||||
const token = useToken(searchQuery)
|
useTokenByAddressAndAutomaticallyAdd(searchQuery)
|
||||||
const [temporaryToken, setTemporaryToken] = useState<Token | null>()
|
|
||||||
|
|
||||||
// filters for ordering
|
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||||
const [activeFilter, setActiveFilter] = useState(FILTERS.BALANCES)
|
|
||||||
|
|
||||||
// toggle specific token import view
|
const sortedTokens: Token[] = useMemo(() => {
|
||||||
const [showTokenImport, setShowTokenImport] = useState(false)
|
if (!isTokenView) return []
|
||||||
|
return Object.values(allTokens).sort(tokenComparator)
|
||||||
|
}, [allTokens, isTokenView, tokenComparator])
|
||||||
|
|
||||||
// used to help scanning on results, put token found from input on left
|
const filteredTokens: Token[] = useMemo(() => {
|
||||||
const [identifiedToken, setIdentifiedToken] = useState<Token>()
|
if (!isTokenView) return []
|
||||||
|
return filterTokens(sortedTokens, searchQuery)
|
||||||
|
}, [isTokenView, sortedTokens, searchQuery])
|
||||||
|
|
||||||
useEffect(() => {
|
function _onTokenSelect(address: string) {
|
||||||
const address = isAddress(searchQuery)
|
|
||||||
if (address && !token) {
|
|
||||||
let stale = false
|
|
||||||
fetchTokenByAddress(address).then(token => {
|
|
||||||
if (!stale) {
|
|
||||||
setTemporaryToken(token)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
stale = true
|
|
||||||
setTemporaryToken(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [searchQuery, token, fetchTokenByAddress])
|
|
||||||
|
|
||||||
// reset view on close
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) {
|
|
||||||
setShowTokenImport(false)
|
|
||||||
}
|
|
||||||
}, [isOpen])
|
|
||||||
|
|
||||||
const tokenList = useMemo(() => {
|
|
||||||
return Object.keys(allTokens)
|
|
||||||
.sort((tokenAddressA, tokenAddressB): number => {
|
|
||||||
// -1 = a is first
|
|
||||||
// 1 = b is first
|
|
||||||
|
|
||||||
// sort ETH first
|
|
||||||
const a = allTokens[tokenAddressA]
|
|
||||||
const b = allTokens[tokenAddressB]
|
|
||||||
if (a.equals(WETH[chainId])) return -1
|
|
||||||
if (b.equals(WETH[chainId])) return 1
|
|
||||||
|
|
||||||
// sort by balances
|
|
||||||
const balanceA = allBalances[account]?.[tokenAddressA]
|
|
||||||
const balanceB = allBalances[account]?.[tokenAddressB]
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort by symbol
|
|
||||||
return a.symbol.toLowerCase() < b.symbol.toLowerCase() ? -1 : 1
|
|
||||||
})
|
|
||||||
.map(tokenAddress => {
|
|
||||||
const token = allTokens[tokenAddress]
|
|
||||||
return {
|
|
||||||
name: token.name,
|
|
||||||
symbol: token.symbol,
|
|
||||||
address: isAddress(tokenAddress) as string,
|
|
||||||
balance: allBalances?.[account]?.[tokenAddress]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [allTokens, chainId, allBalances, account, invertSearchOrder])
|
|
||||||
|
|
||||||
const filteredTokenList = useMemo(() => {
|
|
||||||
return tokenList.filter(tokenEntry => {
|
|
||||||
const urlAdded = urlAddedTokens?.some(token => token.address === tokenEntry.address)
|
|
||||||
const customAdded = userAddedTokens?.some(token => token.address === tokenEntry.address) && !urlAdded
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
})
|
|
||||||
}, [tokenList, urlAddedTokens, userAddedTokens, showTokenImport, searchQuery])
|
|
||||||
|
|
||||||
function _onTokenSelect(address) {
|
|
||||||
setSearchQuery('')
|
|
||||||
onTokenSelect(address)
|
onTokenSelect(address)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear the input on open
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) setSearchQuery('')
|
||||||
|
}, [isOpen, setSearchQuery])
|
||||||
|
|
||||||
// manage focus on modal show
|
// manage focus on modal show
|
||||||
const inputRef = useRef()
|
const inputRef = useRef<HTMLInputElement>()
|
||||||
function onInput(event) {
|
function onInput(event) {
|
||||||
const input = event.target.value
|
const input = event.target.value
|
||||||
const checksummedInput = isAddress(input)
|
const checksummedInput = isAddress(input)
|
||||||
setSearchQuery(checksummedInput || 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 => {
|
|
||||||
if (searchQueryIsAddress && token.address === isAddress(searchQuery)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if (identifiedTokenByAddress.length > 0) setIdentifiedToken(identifiedTokenByAddress[0])
|
|
||||||
}
|
|
||||||
// try to find an exact match by symbol
|
|
||||||
else {
|
|
||||||
const identifiedTokenBySymbol = Object.values(allTokens).filter(token => {
|
|
||||||
if (token.symbol.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()) return true
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if (identifiedTokenBySymbol.length > 0) setIdentifiedToken(identifiedTokenBySymbol[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setIdentifiedToken(undefined)
|
|
||||||
}
|
|
||||||
}, [allTokens, searchQuery])
|
|
||||||
|
|
||||||
const sortedPairList = useMemo(() => {
|
const sortedPairList = useMemo(() => {
|
||||||
|
if (isTokenView) return []
|
||||||
return allPairs.sort((a, b): number => {
|
return allPairs.sort((a, b): number => {
|
||||||
// sort by balance
|
// sort by balance
|
||||||
const balanceA = allBalances[account]?.[a.liquidityToken.address]
|
const balanceA = allPairBalances[a.liquidityToken.address]
|
||||||
const balanceB = allBalances[account]?.[b.liquidityToken.address]
|
const balanceB = allPairBalances[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
|
return balanceComparator(balanceA, balanceB)
|
||||||
if (balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) {
|
|
||||||
return balanceA.greaterThan(balanceB) ? (!invertSearchOrder ? -1 : 1) : !invertSearchOrder ? 1 : -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
})
|
})
|
||||||
}, [allPairs, allBalances, account, invertSearchOrder])
|
}, [isTokenView, allPairs, allPairBalances])
|
||||||
|
|
||||||
const filteredPairList = useMemo(() => {
|
const filteredPairs = useMemo(() => {
|
||||||
const searchQueryIsAddress = !!isAddress(searchQuery)
|
if (isTokenView) return []
|
||||||
return sortedPairList.filter(pair => {
|
return filterPairs(sortedPairList, searchQuery)
|
||||||
// if there's no search query, hide non-ETH pairs
|
}, [isTokenView, searchQuery, sortedPairList])
|
||||||
if (searchQuery === '') return pair.token0.equals(WETH[chainId]) || pair.token1.equals(WETH[chainId])
|
|
||||||
|
|
||||||
const token0 = pair.token0
|
const selectPair = useCallback(
|
||||||
const token1 = pair.token1
|
(pair: Pair) => {
|
||||||
|
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
||||||
|
},
|
||||||
|
[history]
|
||||||
|
)
|
||||||
|
|
||||||
if (searchQueryIsAddress) {
|
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
||||||
if (token0.address === isAddress(searchQuery)) return true
|
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
||||||
if (token1.address === isAddress(searchQuery)) return true
|
})[0]
|
||||||
} 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])
|
|
||||||
|
|
||||||
function renderPairsList() {
|
const openTooltip = useCallback(() => {
|
||||||
if (filteredPairList?.length === 0) {
|
setTooltipOpen(true)
|
||||||
return (
|
inputRef.current?.focus()
|
||||||
<PaddedColumn justify="center">
|
}, [setTooltipOpen])
|
||||||
<Text>No Pools Found</Text>
|
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||||
</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 (temporaryToken === undefined) {
|
|
||||||
return <TokenModalInfo>Searching for Token...</TokenModalInfo>
|
|
||||||
} else if (temporaryToken === null) {
|
|
||||||
return <TokenModalInfo>Address is not a valid ERC-20 token.</TokenModalInfo>
|
|
||||||
} else {
|
|
||||||
// a user found a token by search that isn't yet added to localstorage
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
key={temporaryToken.address}
|
|
||||||
className={`temporary-token-${temporaryToken}`}
|
|
||||||
onClick={() => {
|
|
||||||
addToken(temporaryToken)
|
|
||||||
_onTokenSelect(temporaryToken.address)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RowFixed>
|
|
||||||
<TokenLogo address={temporaryToken.address} size={'24px'} style={{ marginRight: '14px' }} />
|
|
||||||
<Column>
|
|
||||||
<Text fontWeight={500}>{temporaryToken.symbol}</Text>
|
|
||||||
<FadedSpan>(Found by search)</FadedSpan>
|
|
||||||
</Column>
|
|
||||||
</RowFixed>
|
|
||||||
</MenuItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return <TokenModalInfo>{t('noToken')}</TokenModalInfo>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return filteredTokenList.map(({ address, symbol, balance }) => {
|
|
||||||
const urlAdded = urlAddedTokens?.some(token => token.address === address)
|
|
||||||
const customAdded = userAddedTokens?.some(token => token.address === address) && !urlAdded
|
|
||||||
|
|
||||||
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}>
|
|
||||||
{urlAdded && 'Added by URL'}
|
|
||||||
{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 Filter = ({ title, filter, filterType }: { title: string; filter: string; filterType: string }) => {
|
|
||||||
return (
|
|
||||||
<FilterWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setActiveFilter(filter)
|
|
||||||
setInvertSearchOrder(invertSearchOrder => !invertSearchOrder)
|
|
||||||
}}
|
|
||||||
selected={filter === activeFilter}
|
|
||||||
>
|
|
||||||
<Text fontSize={14} fontWeight={500}>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
{filter === activeFilter && filterType === 'tokens' && (
|
|
||||||
<Text fontSize={14} fontWeight={500}>
|
|
||||||
{!invertSearchOrder ? '↓' : '↑'}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</FilterWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} initialFocusRef={isMobile ? undefined : inputRef}>
|
||||||
isOpen={isOpen}
|
|
||||||
onDismiss={clearInputAndDismiss}
|
|
||||||
maxHeight={70}
|
|
||||||
initialFocusRef={isMobile ? undefined : inputRef}
|
|
||||||
>
|
|
||||||
<Column style={{ width: '100%' }}>
|
<Column style={{ width: '100%' }}>
|
||||||
{showTokenImport ? (
|
<PaddedColumn gap="20px">
|
||||||
<PaddedColumn gap="lg">
|
<RowBetween>
|
||||||
<RowBetween>
|
<Text fontWeight={500} fontSize={16}>
|
||||||
<RowFixed>
|
{isTokenView ? 'Select a token' : 'Select a pool'}
|
||||||
<CursorPointer>
|
<QuestionHelper
|
||||||
<ArrowLeft
|
disabled={tooltipOpen}
|
||||||
onClick={() => {
|
text={
|
||||||
setShowTokenImport(false)
|
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.'
|
||||||
</CursorPointer>
|
}
|
||||||
<Text fontWeight={500} fontSize={16} marginLeft={'10px'}>
|
/>
|
||||||
Import A Token
|
</Text>
|
||||||
</Text>
|
<CloseIcon onClick={onDismiss} />
|
||||||
</RowFixed>
|
</RowBetween>
|
||||||
<CloseIcon onClick={onDismiss} />
|
<Tooltip
|
||||||
</RowBetween>
|
text="Import any token into your list by pasting the token address into the search field."
|
||||||
<TYPE.body style={{ marginTop: '10px' }}>
|
showPopup={tooltipOpen}
|
||||||
To import a custom token, paste token address in the search bar.
|
>
|
||||||
</TYPE.body>
|
<SearchInput
|
||||||
<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
|
|
||||||
type={'text'}
|
type={'text'}
|
||||||
id="token-search-input"
|
id="token-search-input"
|
||||||
placeholder={t('tokenSearchPlaceholder')}
|
placeholder={t('tokenSearchPlaceholder')}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onChange={onInput}
|
onChange={onInput}
|
||||||
|
onBlur={closeTooltip}
|
||||||
/>
|
/>
|
||||||
{showCommonBases && (
|
</Tooltip>
|
||||||
<AutoColumn gap="md">
|
{showCommonBases && (
|
||||||
<AutoRow>
|
<CommonBases chainId={chainId} onSelect={_onTokenSelect} selectedTokenAddress={hiddenToken} />
|
||||||
<Text fontWeight={500} fontSize={16}>
|
)}
|
||||||
Common Bases
|
<RowBetween>
|
||||||
</Text>
|
<Text fontSize={14} fontWeight={500}>
|
||||||
<QuestionHelper text="These tokens are commonly used in pairs." />
|
{isTokenView ? 'Token Name' : 'Pool Name'}
|
||||||
</AutoRow>
|
</Text>
|
||||||
<AutoRow gap="10px">
|
{isTokenView && (
|
||||||
{COMMON_BASES[chainId]?.map(token => {
|
<SortButton
|
||||||
return (
|
ascending={invertSearchOrder}
|
||||||
<BaseWrapper
|
toggleSortOrder={() => setInvertSearchOrder(iso => !iso)}
|
||||||
gap="6px"
|
title="Your Balances"
|
||||||
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>
|
|
||||||
)}
|
|
||||||
<RowBetween>
|
|
||||||
<Text fontSize={14} fontWeight={500}>
|
|
||||||
{filterType === 'tokens' ? 'Token Name' : 'Pool Name'}
|
|
||||||
</Text>
|
|
||||||
<Filter
|
|
||||||
title={filterType === 'tokens' ? 'Your Balances' : ' '}
|
|
||||||
filter={FILTERS.BALANCES}
|
|
||||||
filterType={filterType}
|
|
||||||
/>
|
/>
|
||||||
</RowBetween>
|
)}
|
||||||
</PaddedColumn>
|
</RowBetween>
|
||||||
)}
|
</PaddedColumn>
|
||||||
{!showTokenImport && <div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />}
|
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||||
{!showTokenImport && <ItemList>{filterType === 'tokens' ? renderTokenList() : renderPairsList()}</ItemList>}
|
{isTokenView ? (
|
||||||
{!showTokenImport && <div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />}
|
<TokenList
|
||||||
{!showTokenImport && (
|
tokens={filteredTokens}
|
||||||
<Card>
|
allTokenBalances={allTokenBalances}
|
||||||
<AutoRow justify={'center'}>
|
onRemoveAddedToken={removeTokenByAddress}
|
||||||
<div>
|
onTokenSelect={_onTokenSelect}
|
||||||
{filterType !== 'tokens' && (
|
otherSelectedText={otherSelectedText}
|
||||||
<Text fontWeight={500}>
|
otherToken={otherSelectedTokenAddress}
|
||||||
{!isMobile && "Don't see a pool? "}
|
selectedToken={hiddenToken}
|
||||||
<StyledLink
|
showSendWithSwap={showSendWithSwap}
|
||||||
onClick={() => {
|
/>
|
||||||
history.push('/find')
|
) : (
|
||||||
}}
|
<PairList
|
||||||
>
|
pairs={filteredPairs}
|
||||||
{!isMobile ? 'Import it.' : 'Import pool.'}
|
focusTokenAddress={focusedToken?.address}
|
||||||
</StyledLink>
|
onAddLiquidity={selectPair}
|
||||||
</Text>
|
onSelectPair={selectPair}
|
||||||
)}
|
pairBalances={allPairBalances}
|
||||||
{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 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 importing 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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AutoRow>
|
||||||
|
</Card>
|
||||||
</Column>
|
</Column>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
56
src/components/SearchModal/sorting.ts
Normal file
56
src/components/SearchModal/sorting.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||||
|
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 }
|
||||||
|
): (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]
|
||||||
|
|
||||||
|
const balanceComp = balanceComparator(balanceA, balanceB)
|
||||||
|
if (balanceComp !== 0) return balanceComp
|
||||||
|
|
||||||
|
// sort by symbol
|
||||||
|
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
|
||||||
|
const { account, chainId } = useActiveWeb3React()
|
||||||
|
const weth = WETH[chainId]
|
||||||
|
const balances = useAllTokenBalancesTreatingWETHasETH()
|
||||||
|
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])
|
||||||
|
}
|
||||||
95
src/components/SearchModal/styleds.tsx
Normal file
95
src/components/SearchModal/styleds.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import styled from 'styled-components'
|
||||||
|
import { Spinner } from '../../theme'
|
||||||
|
import { AutoColumn } from '../Column'
|
||||||
|
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||||
|
|
||||||
|
export const ModalInfo = styled.div`
|
||||||
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1rem;
|
||||||
|
margin: 0.25rem 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
|
min-height: 200px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const FadedSpan = styled(RowFixed)`
|
||||||
|
color: ${({ theme }) => theme.primary1};
|
||||||
|
font-size: 14px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GreySpan = styled.span`
|
||||||
|
color: ${({ theme }) => theme.text3};
|
||||||
|
font-weight: 400;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const SpinnerWrapper = styled(Spinner)`
|
||||||
|
margin: 0 0.25rem 0 0.25rem;
|
||||||
|
color: ${({ theme }) => theme.text4};
|
||||||
|
opacity: 0.6;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Input = styled.input`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: ${({ theme }) => theme.text1};
|
||||||
|
border-style: solid;
|
||||||
|
border: 1px solid ${({ theme }) => theme.bg3};
|
||||||
|
-webkit-appearance: none;
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: ${({ theme }) => theme.text3};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const PaddedColumn = styled(AutoColumn)`
|
||||||
|
padding: 20px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PaddedItem = styled(RowBetween)`
|
||||||
|
padding: 4px 20px;
|
||||||
|
height: 56px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const MenuItem = styled(PaddedItem)`
|
||||||
|
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 BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
|
||||||
|
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 120px;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
cursor: ${({ disable }) => !disable && 'pointer'};
|
||||||
|
background-color: ${({ theme, disable }) => !disable && theme.bg2};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback, useContext } from 'react'
|
import React, { useState, useEffect, useRef, useCallback, useContext } from 'react'
|
||||||
import styled, { ThemeContext } from 'styled-components'
|
import styled, { ThemeContext } from 'styled-components'
|
||||||
|
|
||||||
import QuestionHelper from '../Question'
|
import QuestionHelper from '../QuestionHelper'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
import { TYPE } from '../../theme'
|
import { TYPE } from '../../theme'
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useTokenWarningDismissal } from '../../state/user/hooks'
|
|||||||
import { Link, TYPE } from '../../theme'
|
import { Link, TYPE } from '../../theme'
|
||||||
import { getEtherscanLink } from '../../utils'
|
import { getEtherscanLink } from '../../utils'
|
||||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||||
import QuestionHelper from '../Question'
|
import QuestionHelper from '../QuestionHelper'
|
||||||
import TokenLogo from '../TokenLogo'
|
import TokenLogo from '../TokenLogo'
|
||||||
|
|
||||||
const Wrapper = styled.div<{ error: boolean }>`
|
const Wrapper = styled.div<{ error: boolean }>`
|
||||||
|
|||||||
18
src/components/Tooltip/index.tsx
Normal file
18
src/components/Tooltip/index.tsx
Normal 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} />
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { CursorPointer, TYPE } from '../../theme'
|
|||||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
|
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
import { SectionBreak } from './styleds'
|
import { SectionBreak } from './styleds'
|
||||||
import QuestionHelper from '../Question'
|
import QuestionHelper from '../QuestionHelper'
|
||||||
import { RowBetween, RowFixed } from '../Row'
|
import { RowBetween, RowFixed } from '../Row'
|
||||||
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
|
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
|
||||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { TYPE } from '../../theme'
|
|||||||
import { formatExecutionPrice } from '../../utils/prices'
|
import { formatExecutionPrice } from '../../utils/prices'
|
||||||
import { ButtonError } from '../Button'
|
import { ButtonError } from '../Button'
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
import QuestionHelper from '../Question'
|
import QuestionHelper from '../QuestionHelper'
|
||||||
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||||
import { StyledBalanceMaxMini } from './styleds'
|
import { StyledBalanceMaxMini } from './styleds'
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export default function CreatePool({ history, location }: RouteComponentProps) {
|
|||||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||||
{token0?.symbol}{' '}
|
{token0?.symbol}{' '}
|
||||||
</Text>
|
</Text>
|
||||||
{console.log(token0)}
|
|
||||||
<TYPE.darkGray fontWeight={500} fontSize={16} marginLeft={'8px'}>
|
<TYPE.darkGray fontWeight={500} fontSize={16} marginLeft={'8px'}>
|
||||||
{token0?.address === WETH[chainId]?.address && '(default)'}
|
{token0?.address === WETH[chainId]?.address && '(default)'}
|
||||||
</TYPE.darkGray>
|
</TYPE.darkGray>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import styled, { ThemeContext } from 'styled-components'
|
|||||||
import { JSBI, Pair } from '@uniswap/sdk'
|
import { JSBI, Pair } from '@uniswap/sdk'
|
||||||
import { RouteComponentProps } from 'react-router-dom'
|
import { RouteComponentProps } from 'react-router-dom'
|
||||||
|
|
||||||
import Question from '../../components/Question'
|
import Question from '../../components/QuestionHelper'
|
||||||
import SearchModal from '../../components/SearchModal'
|
import SearchModal from '../../components/SearchModal'
|
||||||
import PositionCard from '../../components/PositionCard'
|
import PositionCard from '../../components/PositionCard'
|
||||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Card, { BlueCard, GreyCard } from '../../components/Card'
|
|||||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||||
import QuestionHelper from '../../components/Question'
|
import QuestionHelper from '../../components/QuestionHelper'
|
||||||
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
|
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
|
||||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||||
@@ -370,7 +370,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
|||||||
</ArrowWrapper>
|
</ArrowWrapper>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
onClick={() => setSendingWithSwap(false)}
|
onClick={() => setSendingWithSwap(false)}
|
||||||
style={{ marginRight: '0px', width: 'fit-content', fontSize: '14px' }}
|
style={{ marginRight: '0px', width: 'auto', fontSize: '14px' }}
|
||||||
padding={'4px 6px'}
|
padding={'4px 6px'}
|
||||||
>
|
>
|
||||||
Remove Swap
|
Remove Swap
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Card, { GreyCard } from '../../components/Card'
|
|||||||
import { AutoColumn } from '../../components/Column'
|
import { AutoColumn } from '../../components/Column'
|
||||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||||
import QuestionHelper from '../../components/Question'
|
import QuestionHelper from '../../components/QuestionHelper'
|
||||||
import { RowBetween, RowFixed } from '../../components/Row'
|
import { RowBetween, RowFixed } from '../../components/Row'
|
||||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useActiveWeb3React } from '../../hooks'
|
|||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
|
||||||
import { useAllTokens } from '../../hooks/Tokens'
|
import { useAllTokens } from '../../hooks/Tokens'
|
||||||
import { getTokenDecimals, getTokenName, getTokenSymbol, isAddress } from '../../utils'
|
import { getTokenInfoWithFallback, isAddress } from '../../utils'
|
||||||
import { AppDispatch, AppState } from '../index'
|
import { AppDispatch, AppState } from '../index'
|
||||||
import {
|
import {
|
||||||
addSerializedPair,
|
addSerializedPair,
|
||||||
@@ -69,11 +69,7 @@ export function useFetchTokenByAddress(): (address: string) => Promise<Token | n
|
|||||||
if (!library || !chainId) return null
|
if (!library || !chainId) return null
|
||||||
const validatedAddress = isAddress(address)
|
const validatedAddress = isAddress(address)
|
||||||
if (!validatedAddress) return null
|
if (!validatedAddress) return null
|
||||||
const [decimals, symbol, name] = await Promise.all([
|
const { name, symbol, decimals } = await getTokenInfoWithFallback(validatedAddress, library)
|
||||||
getTokenDecimals(address, library).catch(() => null),
|
|
||||||
getTokenSymbol(address, library).catch(() => 'UNKNOWN'),
|
|
||||||
getTokenName(address, library).catch(() => 'Unknown')
|
|
||||||
])
|
|
||||||
|
|
||||||
if (decimals === null) {
|
if (decimals === null) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -60,15 +60,11 @@ const StyledLink = styled.a`
|
|||||||
|
|
||||||
export function Link({
|
export function Link({
|
||||||
onClick,
|
onClick,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
as,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
ref,
|
|
||||||
target = '_blank',
|
target = '_blank',
|
||||||
href,
|
href,
|
||||||
rel = 'noopener noreferrer',
|
rel = 'noopener noreferrer',
|
||||||
...rest
|
...rest
|
||||||
}: HTMLProps<HTMLAnchorElement>) {
|
}: Omit<HTMLProps<HTMLAnchorElement>, 'as' | 'ref'>) {
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
onClick && onClick(event) // first call back into the original onClick
|
onClick && onClick(event) // first call back into the original onClick
|
||||||
|
|||||||
@@ -102,42 +102,49 @@ export function getExchangeContract(pairAddress: string, library: Web3Provider,
|
|||||||
return getContract(pairAddress, IUniswapV2PairABI, library, account)
|
return getContract(pairAddress, IUniswapV2PairABI, library, account)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get token name
|
// get token info and fall back to unknown if not available, except for the
|
||||||
export async function getTokenName(tokenAddress: string, library: Web3Provider) {
|
// 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)) {
|
if (!isAddress(tokenAddress)) {
|
||||||
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
|
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getContract(tokenAddress, ERC20_ABI, library)
|
const token = getContract(tokenAddress, ERC20_ABI, library)
|
||||||
.name()
|
|
||||||
.catch(() =>
|
|
||||||
getContract(tokenAddress, ERC20_BYTES32_ABI, library)
|
|
||||||
.name()
|
|
||||||
.then(parseBytes32String)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get token symbol
|
const namePromise: Promise<string> = token.name().catch(() =>
|
||||||
export async function getTokenSymbol(tokenAddress: string, library: Web3Provider) {
|
getContract(tokenAddress, ERC20_BYTES32_ABI, library)
|
||||||
if (!isAddress(tokenAddress)) {
|
.name()
|
||||||
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
|
.then(parseBytes32String)
|
||||||
}
|
.catch((e: Error) => {
|
||||||
|
console.debug('Failed to get name for token address', e, tokenAddress)
|
||||||
|
return 'Unknown'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
return getContract(tokenAddress, ERC20_ABI, library)
|
const symbolPromise: Promise<string> = token.symbol().catch(() => {
|
||||||
.symbol()
|
const contractBytes32 = getContract(tokenAddress, ERC20_BYTES32_ABI, library)
|
||||||
.catch(() => {
|
return contractBytes32
|
||||||
const contractBytes32 = getContract(tokenAddress, ERC20_BYTES32_ABI, library)
|
.symbol()
|
||||||
return contractBytes32.symbol().then(parseBytes32String)
|
.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
|
const [name, symbol, decimals]: [string, string, number | null] = (await Promise.all([
|
||||||
export async function getTokenDecimals(tokenAddress: string, library: Web3Provider) {
|
namePromise,
|
||||||
if (!isAddress(tokenAddress)) {
|
symbolPromise,
|
||||||
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
|
decimalsPromise
|
||||||
}
|
])) as [string, string, number | null]
|
||||||
|
return { name, symbol, decimals }
|
||||||
return getContract(tokenAddress, ERC20_ABI, library).decimals()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function escapeRegExp(string: string): string {
|
export function escapeRegExp(string: string): string {
|
||||||
|
|||||||
52
yarn.lock
52
yarn.lock
@@ -2385,11 +2385,6 @@
|
|||||||
penpal "3.0.7"
|
penpal "3.0.7"
|
||||||
pocket-js-core "0.0.3"
|
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":
|
"@reach/component-component@^0.1.3":
|
||||||
version "0.1.3"
|
version "0.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/component-component/-/component-component-0.1.3.tgz#5d156319572dc38995b246f81878bc2577c517e5"
|
resolved "https://registry.yarnpkg.com/@reach/component-component/-/component-component-0.1.3.tgz#5d156319572dc38995b246f81878bc2577c517e5"
|
||||||
@@ -2406,11 +2401,6 @@
|
|||||||
react-focus-lock "^1.17.7"
|
react-focus-lock "^1.17.7"
|
||||||
react-remove-scroll "^1.0.2"
|
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":
|
"@reach/portal@^0.2.1":
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.2.1.tgz#07720b999e0063a9e179c14dbdc60fd991cfc9fa"
|
resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.2.1.tgz#07720b999e0063a9e179c14dbdc60fd991cfc9fa"
|
||||||
@@ -2418,36 +2408,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@reach/component-component" "^0.1.3"
|
"@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"
|
|
||||||
prop-types "^15.7.2"
|
|
||||||
|
|
||||||
"@reach/utils@^0.2.3":
|
"@reach/utils@^0.2.3":
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.2.3.tgz#820f6a6af4301b4c5065cfc04bb89e6a3d1d723f"
|
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.2.3.tgz#820f6a6af4301b4c5065cfc04bb89e6a3d1d723f"
|
||||||
integrity sha512-zM9rA8jDchr05giMhL95dPeYkK67cBQnIhCVrOKKqgWGsv+2GE/HZqeptvU4zqs0BvIqsThwov+YxVNVh5csTQ==
|
integrity sha512-zM9rA8jDchr05giMhL95dPeYkK67cBQnIhCVrOKKqgWGsv+2GE/HZqeptvU4zqs0BvIqsThwov+YxVNVh5csTQ==
|
||||||
|
|
||||||
"@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==
|
|
||||||
|
|
||||||
"@reduxjs/toolkit@^1.3.5":
|
"@reduxjs/toolkit@^1.3.5":
|
||||||
version "1.3.5"
|
version "1.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.3.5.tgz#37c1ab6de9aa66f95bab25a8e9bd9d8ec3b7b80c"
|
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.3.5.tgz#37c1ab6de9aa66f95bab25a8e9bd9d8ec3b7b80c"
|
||||||
@@ -2950,6 +2915,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@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":
|
"@types/react@*", "@types/react@^16.9.34":
|
||||||
version "16.9.34"
|
version "16.9.34"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.34.tgz#f7d5e331c468f53affed17a8a4d488cd44ea9349"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.34.tgz#f7d5e331c468f53affed17a8a4d488cd44ea9349"
|
||||||
@@ -11887,7 +11859,7 @@ memdown@~3.0.0:
|
|||||||
ltgt "~2.2.0"
|
ltgt "~2.2.0"
|
||||||
safe-buffer "~5.1.1"
|
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"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
|
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
|
||||||
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
|
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
|
||||||
@@ -14684,6 +14656,14 @@ react-use-gesture@^6.0.14:
|
|||||||
resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-6.0.14.tgz#ab2d35ef72a5fb6060a6160eb12568c276f8a4b1"
|
resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-6.0.14.tgz#ab2d35ef72a5fb6060a6160eb12568c276f8a4b1"
|
||||||
integrity sha512-d9cnZJ0DOFd3FIO76J776DyhtbODgbxGKu19lvc1aSNTnRV5EKr9V4Uda188l2Qh0Va3pqWGxEQlw72r2cmnFQ==
|
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:
|
react@^16.13.1:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
|
||||||
|
|||||||
Reference in New Issue
Block a user