Improvement(lists): Switch to multiple active lists (#1237)
* basic support for multiple active lists * start search across inactive lists * store card before list update * basic import flow for inactive tokens * update supported lists * update import flow for address pasting * basic mvp * hide filter if no results * update min heights * update manage view, index tokens on page load * start routing fix for multi hops * switch to input amount comparison on exactOut * start list import view * updated list UI, token search updates, list import flow, surpress popups and warnings * add unsupported tokens * show warning if logged out * update to opyn list * show token details on warning; * make percent logic more clear * remove uneeded comaprisons * move logic to functions for testing * test updates * update list reducer tests * remove unused locals * code cleanup * add unsupported local list * add multi hop disable switch * add GA * fix bug to return multihop no single * update swap details * copy updates * Visual refinements * Further tweaks * copy updates, actual list order * Move settings button * Update all trade views with settings cog * Add better tips, remove darkmode toggle from dropdown * Clean up routing UI * UI tweaks * minor tweaks * copy updates * add local default list, use existing function for trade comparison, disable v1 helper, show inactive/active at once * updated inactive view * remove slippage fix * update output amount return * center button, update search to character threshold * reset add state on back navigation * style tweak on add button * fix bug on search results Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>
This commit is contained in:
parent
74f50f1b7e
commit
267204d98e
@ -3,18 +3,9 @@ describe('Lists', () => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
it('defaults to uniswap list', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#currency-search-selected-list-name').should('contain', 'Uniswap')
|
||||
})
|
||||
|
||||
// @TODO check if default lists are active when we have them
|
||||
it('change list', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#currency-search-change-list-button').click()
|
||||
cy.get('#list-row-tokens-1inch-eth .select-button').click()
|
||||
cy.get('#currency-search-selected-list-name').should('contain', '1inch')
|
||||
cy.get('#currency-search-change-list-button').click()
|
||||
cy.get('#list-row-tokens-uniswap-eth .select-button').click()
|
||||
cy.get('#currency-search-selected-list-name').should('contain', 'Uniswap')
|
||||
cy.get('.list-token-manage-button').click()
|
||||
})
|
||||
})
|
||||
|
BIN
src/assets/images/token-list-logo.png
Normal file
BIN
src/assets/images/token-list-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
@ -16,7 +16,7 @@ const Base = styled(RebassButton)<{
|
||||
width: ${({ width }) => (width ? width : '100%')};
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
border-radius: 20px;
|
||||
border-radius: ${({ borderRadius }) => borderRadius && borderRadius};
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
@ -53,13 +53,15 @@ export const ButtonPrimary = styled(Base)`
|
||||
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
|
||||
}
|
||||
&:disabled {
|
||||
background-color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? theme.primary1 : theme.bg3)};
|
||||
color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? 'white' : theme.text3)};
|
||||
background-color: ${({ theme, altDisabledStyle, disabled }) =>
|
||||
altDisabledStyle ? (disabled ? theme.bg3 : theme.primary1) : theme.bg3};
|
||||
color: ${({ theme, altDisabledStyle, disabled }) =>
|
||||
altDisabledStyle ? (disabled ? theme.text3 : 'white') : theme.text3};
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.7' : '1')};
|
||||
opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.5' : '1')};
|
||||
}
|
||||
`
|
||||
|
||||
@ -97,15 +99,13 @@ export const ButtonGray = styled(Base)`
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg4)};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg4)};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg4)};
|
||||
}
|
||||
`
|
||||
|
||||
@ -210,10 +210,10 @@ export const ButtonEmpty = styled(Base)`
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration: none;
|
||||
}
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
text-decoration: none;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
@ -308,6 +308,17 @@ export function ButtonDropdown({ disabled = false, children, ...rest }: { disabl
|
||||
)
|
||||
}
|
||||
|
||||
export function ButtonDropdownGrey({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonGray {...rest} disabled={disabled} style={{ borderRadius: '20px' }}>
|
||||
<RowBetween>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>{children}</div>
|
||||
<ChevronDown size={24} />
|
||||
</RowBetween>
|
||||
</ButtonGray>
|
||||
)
|
||||
}
|
||||
|
||||
export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonOutlined {...rest} disabled={disabled}>
|
||||
|
@ -3,8 +3,8 @@ import styled from 'styled-components'
|
||||
import { CardProps, Text } from 'rebass'
|
||||
import { Box } from 'rebass/styled-components'
|
||||
|
||||
const Card = styled(Box)<{ padding?: string; border?: string; borderRadius?: string }>`
|
||||
width: 100%;
|
||||
const Card = styled(Box)<{ width?: string; padding?: string; border?: string; borderRadius?: string }>`
|
||||
width: ${({ width }) => width ?? '100%'};
|
||||
border-radius: 16px;
|
||||
padding: 1.25rem;
|
||||
padding: ${({ padding }) => padding};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Currency, Pair } from '@uniswap/sdk'
|
||||
import React, { useState, useContext, useCallback } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { useCurrencyBalance } from '../../state/wallet/hooks'
|
||||
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
|
||||
@ -13,6 +13,7 @@ import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTheme from '../../hooks/useTheme'
|
||||
|
||||
const InputRow = styled.div<{ selected: boolean }>`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
@ -154,7 +155,7 @@ export default function CurrencyInputPanel({
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const { account } = useActiveWeb3React()
|
||||
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
|
||||
const theme = useContext(ThemeContext)
|
||||
const theme = useTheme()
|
||||
|
||||
const handleDismissSearch = useCallback(() => {
|
||||
setModalOpen(false)
|
||||
|
@ -22,6 +22,7 @@ const StyledLogo = styled(Logo)<{ size: string }>`
|
||||
height: ${({ size }) => size};
|
||||
border-radius: ${({ size }) => size};
|
||||
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
|
||||
background-color: ${({ theme }) => theme.white};
|
||||
`
|
||||
|
||||
export default function CurrencyLogo({
|
||||
|
@ -17,7 +17,7 @@ import { CountUp } from 'use-count-up'
|
||||
import { TYPE, ExternalLink } from '../../theme'
|
||||
|
||||
import { YellowCard } from '../Card'
|
||||
import Settings from '../Settings'
|
||||
import { Moon, Sun } from 'react-feather'
|
||||
import Menu from '../Menu'
|
||||
|
||||
import Row, { RowFixed } from '../Row'
|
||||
@ -254,6 +254,35 @@ const StyledExternalLink = styled(ExternalLink).attrs({
|
||||
`}
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 35px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
margin-left: 8px;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
}
|
||||
`
|
||||
|
||||
const NETWORK_LABELS: { [chainId in ChainId]?: string } = {
|
||||
[ChainId.RINKEBY]: 'Rinkeby',
|
||||
[ChainId.ROPSTEN]: 'Ropsten',
|
||||
@ -266,7 +295,8 @@ export default function Header() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const userEthBalance = useETHBalances(account ? [account] : [])?.[account ?? '']
|
||||
const [isDark] = useDarkModeManager()
|
||||
// const [isDark] = useDarkModeManager()
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
const toggleClaimModal = useToggleSelfClaimModal()
|
||||
|
||||
@ -291,7 +321,7 @@ export default function Header() {
|
||||
<HeaderRow>
|
||||
<Title href=".">
|
||||
<UniIcon>
|
||||
<img width={'24px'} src={isDark ? LogoDark : Logo} alt="logo" />
|
||||
<img width={'24px'} src={darkMode ? LogoDark : Logo} alt="logo" />
|
||||
</UniIcon>
|
||||
</Title>
|
||||
<HeaderLinks>
|
||||
@ -375,7 +405,9 @@ export default function Header() {
|
||||
</AccountElement>
|
||||
</HeaderElement>
|
||||
<HeaderElementWrap>
|
||||
<Settings />
|
||||
<StyledMenuButton onClick={() => toggleDarkMode()}>
|
||||
{darkMode ? <Moon size={20} /> : <Sun size={20} />}
|
||||
</StyledMenuButton>
|
||||
<Menu />
|
||||
</HeaderElementWrap>
|
||||
</HeaderControls>
|
||||
|
@ -6,7 +6,11 @@ import { NavLink, Link as HistoryLink } from 'react-router-dom'
|
||||
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { RowBetween } from '../Row'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
// import QuestionHelper from '../QuestionHelper'
|
||||
import Settings from '../Settings'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { AppDispatch } from 'state'
|
||||
import { resetMintState } from 'state/mint/actions'
|
||||
|
||||
const Tabs = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
@ -69,32 +73,34 @@ export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) {
|
||||
export function FindPoolTabs() {
|
||||
return (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<RowBetween style={{ padding: '1rem 1rem 0 1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<StyledArrowLeft />
|
||||
</HistoryLink>
|
||||
<ActiveText>Import Pool</ActiveText>
|
||||
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
|
||||
<Settings />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
export function AddRemoveTabs({ adding, creating }: { adding: boolean; creating: boolean }) {
|
||||
// reset states on back
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
return (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<RowBetween style={{ padding: '1rem 1rem 0 1rem' }}>
|
||||
<HistoryLink
|
||||
to="/pool"
|
||||
onClick={() => {
|
||||
adding && dispatch(resetMintState())
|
||||
}}
|
||||
>
|
||||
<StyledArrowLeft />
|
||||
</HistoryLink>
|
||||
<ActiveText>{creating ? 'Create a pair' : adding ? 'Add Liquidity' : 'Remove Liquidity'}</ActiveText>
|
||||
<QuestionHelper
|
||||
text={
|
||||
adding
|
||||
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
|
||||
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
|
||||
}
|
||||
/>
|
||||
<Settings />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
|
@ -9,10 +9,10 @@ import { useTotalSupply } from '../../data/TotalSupply'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTokenBalance } from '../../state/wallet/hooks'
|
||||
import { ExternalLink, TYPE, HideExtraSmall, ExtraSmallOnly } from '../../theme'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { currencyId } from '../../utils/currencyId'
|
||||
import { unwrappedToken } from '../../utils/wrappedCurrency'
|
||||
import { ButtonPrimary, ButtonSecondary, ButtonEmpty, ButtonUNIGradient } from '../Button'
|
||||
import { ButtonPrimary, ButtonSecondary, ButtonEmpty } from '../Button'
|
||||
import { transparentize } from 'polished'
|
||||
import { CardNoise } from '../earn/styled'
|
||||
|
||||
@ -202,18 +202,7 @@ export default function FullPositionCard({ pair, border, stakedBalance }: Positi
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{!currency0 || !currency1 ? <Dots>Loading</Dots> : `${currency0.symbol}/${currency1.symbol}`}
|
||||
</Text>
|
||||
{!!stakedBalance && (
|
||||
<ButtonUNIGradient as={Link} to={`/uni/${currencyId(currency0)}/${currencyId(currency1)}`}>
|
||||
<HideExtraSmall>Earning UNI</HideExtraSmall>
|
||||
<ExtraSmallOnly>
|
||||
<span role="img" aria-label="bolt">
|
||||
⚡
|
||||
</span>
|
||||
</ExtraSmallOnly>
|
||||
</ButtonUNIGradient>
|
||||
)}
|
||||
</AutoRow>
|
||||
|
||||
<RowFixed gap="8px">
|
||||
<ButtonEmpty
|
||||
padding="6px 8px"
|
||||
|
@ -1,11 +1,19 @@
|
||||
import styled from 'styled-components'
|
||||
import { Box } from 'rebass/styled-components'
|
||||
|
||||
const Row = styled(Box)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>`
|
||||
width: 100%;
|
||||
const Row = styled(Box)<{
|
||||
width?: string
|
||||
align?: string
|
||||
justify?: string
|
||||
padding?: string
|
||||
border?: string
|
||||
borderRadius?: string
|
||||
}>`
|
||||
width: ${({ width }) => width ?? '100%'};
|
||||
display: flex;
|
||||
padding: 0;
|
||||
align-items: ${({ align }) => (align ? align : 'center')};
|
||||
align-items: ${({ align }) => align ?? 'center'};
|
||||
justify-content: ${({ justify }) => justify ?? 'flex-start'};
|
||||
padding: ${({ padding }) => padding};
|
||||
border: ${({ border }) => border};
|
||||
border-radius: ${({ borderRadius }) => borderRadius};
|
||||
|
@ -4,18 +4,19 @@ import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useSelectedTokenList, WrappedTokenInfo } from '../../state/lists/hooks'
|
||||
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { WrappedTokenInfo, useCombinedActiveList } from '../../state/lists/hooks'
|
||||
import { useCurrencyBalance } from '../../state/wallet/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { useIsUserAddedToken } from '../../hooks/Tokens'
|
||||
import { TYPE } from '../../theme'
|
||||
import { useIsUserAddedToken, useAllInactiveTokens } from '../../hooks/Tokens'
|
||||
import Column from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
import { FadedSpan, MenuItem } from './styleds'
|
||||
import { MenuItem } from './styleds'
|
||||
import Loader from '../Loader'
|
||||
import { isTokenOnList } from '../../utils'
|
||||
import ImportRow from './ImportRow'
|
||||
import { wrappedCurrency } from 'utils/wrappedCurrency'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : ''
|
||||
@ -93,16 +94,13 @@ function CurrencyRow({
|
||||
otherSelected: boolean
|
||||
style: CSSProperties
|
||||
}) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const { account } = useActiveWeb3React()
|
||||
const key = currencyKey(currency)
|
||||
const selectedTokenList = useSelectedTokenList()
|
||||
const selectedTokenList = useCombinedActiveList()
|
||||
const isOnSelectedList = isTokenOnList(selectedTokenList, currency)
|
||||
const customAdded = useIsUserAddedToken(currency)
|
||||
const balance = useCurrencyBalance(account ?? undefined, currency)
|
||||
|
||||
const removeToken = useRemoveUserAddedToken()
|
||||
const addToken = useAddUserToken()
|
||||
|
||||
// only show add or remove buttons if not on selected list
|
||||
return (
|
||||
<MenuItem
|
||||
@ -117,34 +115,9 @@ function CurrencyRow({
|
||||
<Text title={currency.name} fontWeight={500}>
|
||||
{currency.symbol}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
{!isOnSelectedList && customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Added by user
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (chainId && currency instanceof Token) removeToken(chainId, currency.address)
|
||||
}}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
{!isOnSelectedList && !customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Found by address
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (currency instanceof Token) addToken(currency)
|
||||
}}
|
||||
>
|
||||
(Add)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
</FadedSpan>
|
||||
<TYPE.darkGray ml="0px" fontSize={'12px'} fontWeight={300}>
|
||||
{currency.name} {!isOnSelectedList && customAdded && '• Added by user'}
|
||||
</TYPE.darkGray>
|
||||
</Column>
|
||||
<TokenTags currency={currency} />
|
||||
<RowFixed style={{ justifySelf: 'flex-end' }}>
|
||||
@ -161,7 +134,9 @@ export default function CurrencyList({
|
||||
onCurrencySelect,
|
||||
otherCurrency,
|
||||
fixedListRef,
|
||||
showETH
|
||||
showETH,
|
||||
showImportView,
|
||||
setImportToken
|
||||
}: {
|
||||
height: number
|
||||
currencies: Currency[]
|
||||
@ -170,26 +145,51 @@ export default function CurrencyList({
|
||||
otherCurrency?: Currency | null
|
||||
fixedListRef?: MutableRefObject<FixedSizeList | undefined>
|
||||
showETH: boolean
|
||||
showImportView: () => void
|
||||
setImportToken: (token: Token) => void
|
||||
}) {
|
||||
const itemData = useMemo(() => (showETH ? [Currency.ETHER, ...currencies] : currencies), [currencies, showETH])
|
||||
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const inactiveTokens: {
|
||||
[address: string]: Token
|
||||
} = useAllInactiveTokens()
|
||||
|
||||
const Row = useCallback(
|
||||
({ data, index, style }) => {
|
||||
const currency: Currency = data[index]
|
||||
const isSelected = Boolean(selectedCurrency && currencyEquals(selectedCurrency, currency))
|
||||
const otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency))
|
||||
const handleSelect = () => onCurrencySelect(currency)
|
||||
return (
|
||||
<CurrencyRow
|
||||
style={style}
|
||||
currency={currency}
|
||||
isSelected={isSelected}
|
||||
onSelect={handleSelect}
|
||||
otherSelected={otherSelected}
|
||||
/>
|
||||
)
|
||||
|
||||
const token = wrappedCurrency(currency, chainId)
|
||||
|
||||
const showImport = inactiveTokens && token && Object.keys(inactiveTokens).includes(token.address)
|
||||
|
||||
if (showImport && token) {
|
||||
return (
|
||||
<ImportRow
|
||||
style={style}
|
||||
token={token}
|
||||
showImportView={showImportView}
|
||||
setImportToken={setImportToken}
|
||||
dim={true}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<CurrencyRow
|
||||
style={style}
|
||||
currency={currency}
|
||||
isSelected={isSelected}
|
||||
onSelect={handleSelect}
|
||||
otherSelected={otherSelected}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
[onCurrencySelect, otherCurrency, selectedCurrency]
|
||||
[chainId, inactiveTokens, onCurrencySelect, otherCurrency, selectedCurrency, setImportToken, showImportView]
|
||||
)
|
||||
|
||||
const itemKey = useCallback((index: number, data: any) => currencyKey(data[index]), [])
|
||||
|
@ -1,27 +1,44 @@
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { KeyboardEvent, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import { useSelectedListInfo } from '../../state/lists/hooks'
|
||||
import { CloseIcon, LinkStyledButton, TYPE } from '../../theme'
|
||||
import { useAllTokens, useToken, useIsUserAddedToken, useFoundOnInactiveList } from '../../hooks/Tokens'
|
||||
import { CloseIcon, TYPE, ButtonText, IconWrapper } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import Card from '../Card'
|
||||
import Column from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import Row, { RowBetween, RowFixed } from '../Row'
|
||||
import CommonBases from './CommonBases'
|
||||
import CurrencyList from './CurrencyList'
|
||||
import { filterTokens } from './filtering'
|
||||
import SortButton from './SortButton'
|
||||
import { useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput, Separator } from './styleds'
|
||||
import AutoSizer from 'react-virtualized-auto-sizer'
|
||||
import styled from 'styled-components'
|
||||
import useToggle from 'hooks/useToggle'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import ImportRow from './ImportRow'
|
||||
import { Edit } from 'react-feather'
|
||||
import { ButtonLight } from 'components/Button'
|
||||
|
||||
const ContentWrapper = styled(Column)`
|
||||
width: 100%;
|
||||
flex: 1 1;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const Footer = styled.div`
|
||||
width: 100%;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
border-top: 1px solid ${({ theme }) => theme.bg2};
|
||||
`
|
||||
|
||||
interface CurrencySearchProps {
|
||||
isOpen: boolean
|
||||
@ -30,7 +47,9 @@ interface CurrencySearchProps {
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherSelectedCurrency?: Currency | null
|
||||
showCommonBases?: boolean
|
||||
onChangeList: () => void
|
||||
showManageView: () => void
|
||||
showImportView: () => void
|
||||
setImportToken: (token: Token) => void
|
||||
}
|
||||
|
||||
export function CurrencySearch({
|
||||
@ -40,20 +59,27 @@ export function CurrencySearch({
|
||||
showCommonBases,
|
||||
onDismiss,
|
||||
isOpen,
|
||||
onChangeList
|
||||
showManageView,
|
||||
showImportView,
|
||||
setImportToken
|
||||
}: CurrencySearchProps) {
|
||||
const { t } = useTranslation()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
const theme = useTheme()
|
||||
|
||||
// refs for fixed size lists
|
||||
const fixedList = useRef<FixedSizeList>()
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
const [invertSearchOrder] = useState<boolean>(false)
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
// const inactiveTokens: Token[] | undefined = useFoundOnInactiveList(searchQuery)
|
||||
|
||||
// if they input an address, use it
|
||||
const isAddressSearch = isAddress(searchQuery)
|
||||
const searchToken = useToken(searchQuery)
|
||||
const searchTokenIsAdded = useIsUserAddedToken(searchToken)
|
||||
|
||||
useEffect(() => {
|
||||
if (isAddressSearch) {
|
||||
@ -73,26 +99,39 @@ export function CurrencySearch({
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (isAddressSearch) return searchToken ? [searchToken] : []
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [isAddressSearch, searchToken, allTokens, searchQuery])
|
||||
}, [allTokens, searchQuery])
|
||||
|
||||
const filteredSortedTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
const sorted = filteredTokens.sort(tokenComparator)
|
||||
const symbolMatch = searchQuery
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
if (symbolMatch.length > 1) return sorted
|
||||
|
||||
if (symbolMatch.length > 1) {
|
||||
return sorted
|
||||
}
|
||||
|
||||
return [
|
||||
...(searchToken ? [searchToken] : []),
|
||||
// sort any exact symbol matches first
|
||||
...sorted.filter(token => token.symbol?.toLowerCase() === symbolMatch[0]),
|
||||
...sorted.filter(token => token.symbol?.toLowerCase() !== symbolMatch[0])
|
||||
|
||||
// sort by tokens whos symbols start with search substrng
|
||||
...sorted.filter(
|
||||
token =>
|
||||
token.symbol?.toLowerCase().startsWith(searchQuery.toLowerCase().trim()) &&
|
||||
token.symbol?.toLowerCase() !== symbolMatch[0]
|
||||
),
|
||||
|
||||
// rest that dont match upove
|
||||
...sorted.filter(
|
||||
token =>
|
||||
!token.symbol?.toLowerCase().startsWith(searchQuery.toLowerCase().trim()) &&
|
||||
token.symbol?.toLowerCase() !== symbolMatch[0]
|
||||
)
|
||||
]
|
||||
}, [filteredTokens, searchQuery, searchToken, tokenComparator])
|
||||
}, [filteredTokens, searchQuery, tokenComparator])
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
@ -135,80 +174,130 @@ export function CurrencySearch({
|
||||
[filteredSortedTokens, handleCurrencySelect, searchQuery]
|
||||
)
|
||||
|
||||
const selectedListInfo = useSelectedListInfo()
|
||||
// menu ui
|
||||
const [open, toggle] = useToggle(false)
|
||||
const node = useRef<HTMLDivElement>()
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
// if no results on main list, show option to expand into inactive
|
||||
const [showExpanded, setShowExpanded] = useState(false)
|
||||
const inactiveTokens = useFoundOnInactiveList(searchQuery)
|
||||
|
||||
// reset expanded results on query reset
|
||||
useEffect(() => {
|
||||
if (searchQuery === '') {
|
||||
setShowExpanded(false)
|
||||
}
|
||||
}, [setShowExpanded, searchQuery])
|
||||
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn gap="14px">
|
||||
<ContentWrapper>
|
||||
<PaddedColumn gap="16px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a token
|
||||
<QuestionHelper text="Find a token by searching for its name or symbol or by pasting its address below." />
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnter}
|
||||
/>
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
autoComplete="off"
|
||||
value={searchQuery}
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnter}
|
||||
/>
|
||||
</Row>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={selectedCurrency} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Token Name
|
||||
</Text>
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
{searchToken && !searchTokenIsAdded ? (
|
||||
<Column style={{ padding: '20px 0', height: '100%' }}>
|
||||
<ImportRow token={searchToken} showImportView={showImportView} setImportToken={setImportToken} />
|
||||
</Column>
|
||||
) : filteredSortedTokens?.length > 0 || (showExpanded && inactiveTokens && inactiveTokens.length > 0) ? (
|
||||
<div style={{ flex: '1' }}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<CurrencyList
|
||||
height={height}
|
||||
showETH={showETH}
|
||||
currencies={
|
||||
showExpanded && inactiveTokens ? filteredSortedTokens.concat(inactiveTokens) : filteredSortedTokens
|
||||
}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
otherCurrency={otherSelectedCurrency}
|
||||
selectedCurrency={selectedCurrency}
|
||||
fixedListRef={fixedList}
|
||||
showImportView={showImportView}
|
||||
setImportToken={setImportToken}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
) : (
|
||||
<Column style={{ padding: '20px', height: '100%' }}>
|
||||
<TYPE.main color={theme.text3} textAlign="center" mb="20px">
|
||||
No results found in active lists.
|
||||
</TYPE.main>
|
||||
{inactiveTokens &&
|
||||
inactiveTokens.length > 0 &&
|
||||
!(searchToken && !searchTokenIsAdded) &&
|
||||
searchQuery.length > 1 &&
|
||||
filteredSortedTokens?.length === 0 && (
|
||||
// expand button in line with no results
|
||||
<Row align="center" width="100%" justify="center">
|
||||
<ButtonLight
|
||||
width="fit-content"
|
||||
borderRadius="12px"
|
||||
padding="8px 12px"
|
||||
onClick={() => setShowExpanded(!showExpanded)}
|
||||
>
|
||||
{!showExpanded
|
||||
? `Show ${inactiveTokens.length} more inactive ${inactiveTokens.length === 1 ? 'token' : 'tokens'}`
|
||||
: 'Hide expanded search'}
|
||||
</ButtonLight>
|
||||
</Row>
|
||||
)}
|
||||
</Column>
|
||||
)}
|
||||
|
||||
<div style={{ flex: '1' }}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<CurrencyList
|
||||
height={height}
|
||||
showETH={showETH}
|
||||
currencies={filteredSortedTokens}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
otherCurrency={otherSelectedCurrency}
|
||||
selectedCurrency={selectedCurrency}
|
||||
fixedListRef={fixedList}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Card>
|
||||
<RowBetween>
|
||||
{selectedListInfo.current ? (
|
||||
<Row>
|
||||
{selectedListInfo.current.logoURI ? (
|
||||
<ListLogo
|
||||
style={{ marginRight: 12 }}
|
||||
logoURI={selectedListInfo.current.logoURI}
|
||||
alt={`${selectedListInfo.current.name} list logo`}
|
||||
/>
|
||||
) : null}
|
||||
<TYPE.main id="currency-search-selected-list-name">{selectedListInfo.current.name}</TYPE.main>
|
||||
</Row>
|
||||
) : null}
|
||||
<LinkStyledButton
|
||||
style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }}
|
||||
onClick={onChangeList}
|
||||
id="currency-search-change-list-button"
|
||||
>
|
||||
{selectedListInfo.current ? 'Change' : 'Select a list'}
|
||||
</LinkStyledButton>
|
||||
</RowBetween>
|
||||
</Card>
|
||||
</Column>
|
||||
{inactiveTokens &&
|
||||
inactiveTokens.length > 0 &&
|
||||
!(searchToken && !searchTokenIsAdded) &&
|
||||
(searchQuery.length > 1 || showExpanded) &&
|
||||
(filteredSortedTokens?.length !== 0 || showExpanded) && (
|
||||
// button fixed to bottom
|
||||
<Row align="center" width="100%" justify="center" style={{ position: 'absolute', bottom: '80px', left: 0 }}>
|
||||
<ButtonLight
|
||||
width="fit-content"
|
||||
borderRadius="12px"
|
||||
padding="8px 12px"
|
||||
onClick={() => setShowExpanded(!showExpanded)}
|
||||
>
|
||||
{!showExpanded
|
||||
? `Show ${inactiveTokens.length} more inactive ${inactiveTokens.length === 1 ? 'token' : 'tokens'}`
|
||||
: 'Hide expanded search'}
|
||||
</ButtonLight>
|
||||
</Row>
|
||||
)}
|
||||
<Footer>
|
||||
<Row justify="center">
|
||||
<ButtonText onClick={showManageView} color={theme.blue1} className="list-token-manage-button">
|
||||
<RowFixed>
|
||||
<IconWrapper size="16px" marginRight="6px">
|
||||
<Edit />
|
||||
</IconWrapper>
|
||||
<TYPE.main color={theme.blue1}>Manage</TYPE.main>
|
||||
</RowFixed>
|
||||
</ButtonText>
|
||||
</Row>
|
||||
</Footer>
|
||||
</ContentWrapper>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { Currency } from '@uniswap/sdk'
|
||||
import { Currency, Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import useLast from '../../hooks/useLast'
|
||||
import Modal from '../Modal'
|
||||
import { CurrencySearch } from './CurrencySearch'
|
||||
import { ListSelect } from './ListSelect'
|
||||
import { ImportToken } from './ImportToken'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import Manage from './Manage'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { ImportList } from './ImportList'
|
||||
|
||||
interface CurrencySearchModalProps {
|
||||
isOpen: boolean
|
||||
@ -15,6 +18,13 @@ interface CurrencySearchModalProps {
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
export enum CurrencyModalView {
|
||||
search,
|
||||
manage,
|
||||
importToken,
|
||||
importList
|
||||
}
|
||||
|
||||
export default function CurrencySearchModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
@ -23,12 +33,12 @@ export default function CurrencySearchModal({
|
||||
otherSelectedCurrency,
|
||||
showCommonBases = false
|
||||
}: CurrencySearchModalProps) {
|
||||
const [listView, setListView] = useState<boolean>(false)
|
||||
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.manage)
|
||||
const lastOpen = useLast(isOpen)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !lastOpen) {
|
||||
setListView(false)
|
||||
setModalView(CurrencyModalView.search)
|
||||
}
|
||||
}, [isOpen, lastOpen])
|
||||
|
||||
@ -40,35 +50,54 @@ export default function CurrencySearchModal({
|
||||
[onDismiss, onCurrencySelect]
|
||||
)
|
||||
|
||||
const handleClickChangeList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Change Lists'
|
||||
})
|
||||
setListView(true)
|
||||
}, [])
|
||||
const handleClickBack = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Back'
|
||||
})
|
||||
setListView(false)
|
||||
}, [])
|
||||
// for token import view
|
||||
const prevView = usePrevious(modalView)
|
||||
|
||||
// used for import token flow
|
||||
const [importToken, setImportToken] = useState<Token | undefined>()
|
||||
|
||||
// used for import list
|
||||
const [importList, setImportList] = useState<TokenList | undefined>()
|
||||
const [listURL, setListUrl] = useState<string | undefined>()
|
||||
|
||||
// change min height if not searching
|
||||
const minHeight = modalView === CurrencyModalView.importToken || modalView === CurrencyModalView.importList ? 40 : 80
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={80} minHeight={listView ? 40 : 80}>
|
||||
{listView ? (
|
||||
<ListSelect onDismiss={onDismiss} onBack={handleClickBack} />
|
||||
) : (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={80} minHeight={minHeight}>
|
||||
{modalView === CurrencyModalView.search ? (
|
||||
<CurrencySearch
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
onChangeList={handleClickChangeList}
|
||||
selectedCurrency={selectedCurrency}
|
||||
otherSelectedCurrency={otherSelectedCurrency}
|
||||
showCommonBases={showCommonBases}
|
||||
showImportView={() => setModalView(CurrencyModalView.importToken)}
|
||||
setImportToken={setImportToken}
|
||||
showManageView={() => setModalView(CurrencyModalView.manage)}
|
||||
/>
|
||||
) : modalView === CurrencyModalView.importToken && importToken ? (
|
||||
<ImportToken
|
||||
token={importToken}
|
||||
onDismiss={onDismiss}
|
||||
onBack={() =>
|
||||
setModalView(prevView && prevView !== CurrencyModalView.importToken ? prevView : CurrencyModalView.search)
|
||||
}
|
||||
handleCurrencySelect={handleCurrencySelect}
|
||||
/>
|
||||
) : modalView === CurrencyModalView.importList && importList && listURL ? (
|
||||
<ImportList list={importList} listURL={listURL} onDismiss={onDismiss} setModalView={setModalView} />
|
||||
) : modalView === CurrencyModalView.manage ? (
|
||||
<Manage
|
||||
onDismiss={onDismiss}
|
||||
setModalView={setModalView}
|
||||
setImportToken={setImportToken}
|
||||
setImportList={setImportList}
|
||||
setListUrl={setListUrl}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
|
162
src/components/SearchModal/ImportList.tsx
Normal file
162
src/components/SearchModal/ImportList.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import ReactGA from 'react-ga'
|
||||
import { TYPE, CloseIcon } from 'theme'
|
||||
import Card from 'components/Card'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { RowBetween, RowFixed, AutoRow } from 'components/Row'
|
||||
import { ArrowLeft, AlertTriangle } from 'react-feather'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { transparentize } from 'polished'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import { SectionBreak } from 'components/swap/styleds'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import ListLogo from 'components/ListLogo'
|
||||
import { PaddedColumn, Checkbox, TextDot } from './styleds'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { AppDispatch } from 'state'
|
||||
import { useFetchListCallback } from 'hooks/useFetchListCallback'
|
||||
import { removeList, enableList } from 'state/lists/actions'
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
import { useAllLists } from 'state/lists/hooks'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
interface ImportProps {
|
||||
listURL: string
|
||||
list: TokenList
|
||||
onDismiss: () => void
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
}
|
||||
|
||||
export function ImportList({ listURL, list, setModalView, onDismiss }: ImportProps) {
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
// user must accept
|
||||
const [confirmed, setConfirmed] = useState(false)
|
||||
|
||||
const lists = useAllLists()
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
// monitor is list is loading
|
||||
const adding = Boolean(lists[listURL]?.loadingRequestId)
|
||||
const [addError, setAddError] = useState<string | null>(null)
|
||||
|
||||
const handleAddList = useCallback(() => {
|
||||
if (adding) return
|
||||
setAddError(null)
|
||||
fetchList(listURL)
|
||||
.then(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List',
|
||||
label: listURL
|
||||
})
|
||||
|
||||
// turn list on
|
||||
dispatch(enableList(listURL))
|
||||
// go back to lists
|
||||
setModalView(CurrencyModalView.manage)
|
||||
})
|
||||
.catch(error => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List Failed',
|
||||
label: listURL
|
||||
})
|
||||
setAddError(error.message)
|
||||
dispatch(removeList(listURL))
|
||||
})
|
||||
}, [adding, dispatch, fetchList, listURL, setModalView])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn gap="14px" style={{ width: '100%', flex: '1 1' }}>
|
||||
<RowBetween>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={() => setModalView(CurrencyModalView.manage)} />
|
||||
<TYPE.mediumHeader>Import List</TYPE.mediumHeader>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<SectionBreak />
|
||||
<PaddedColumn gap="md">
|
||||
<AutoColumn gap="md">
|
||||
<Card backgroundColor={theme.bg2} padding="12px 20px">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
{list.logoURI && <ListLogo logoURI={list.logoURI} size="40px" />}
|
||||
<AutoColumn gap="sm" style={{ marginLeft: '20px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.body fontWeight={600} mr="6px">
|
||||
{list.name}
|
||||
</TYPE.body>
|
||||
<TextDot />
|
||||
<TYPE.main fontSize={'16px'} ml="6px">
|
||||
{list.tokens.length} tokens
|
||||
</TYPE.main>
|
||||
</RowFixed>
|
||||
<ExternalLink href={`https://tokenlists.org/token-list?url=${listURL}`}>
|
||||
<TYPE.main fontSize={'12px'} color={theme.blue1}>
|
||||
{listURL}
|
||||
</TYPE.main>
|
||||
</ExternalLink>
|
||||
</AutoColumn>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
</Card>
|
||||
<Card style={{ backgroundColor: transparentize(0.8, theme.red1) }}>
|
||||
<AutoColumn justify="center" style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
|
||||
<AlertTriangle stroke={theme.red1} size={32} />
|
||||
<TYPE.body fontWeight={500} fontSize={20} color={theme.red1}>
|
||||
Import at your own risk{' '}
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
|
||||
<AutoColumn style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
|
||||
<TYPE.body fontWeight={500} color={theme.red1}>
|
||||
By adding this list you are implicitly trusting that the data is correct. Anyone can create a list,
|
||||
including creating fake versions of existing lists and lists that claim to represent projects that do
|
||||
not have one.
|
||||
</TYPE.body>
|
||||
<TYPE.body fontWeight={600} color={theme.red1}>
|
||||
If you purchase a token from this list, you may not be able to sell it back.
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
<AutoRow justify="center" style={{ cursor: 'pointer' }} onClick={() => setConfirmed(!confirmed)}>
|
||||
<Checkbox
|
||||
name="confirmed"
|
||||
type="checkbox"
|
||||
checked={confirmed}
|
||||
onChange={() => setConfirmed(!confirmed)}
|
||||
/>
|
||||
<TYPE.body ml="10px" fontSize="16px" color={theme.red1} fontWeight={500}>
|
||||
I understand
|
||||
</TYPE.body>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
|
||||
<ButtonPrimary
|
||||
disabled={!confirmed}
|
||||
altDisabledStyle={true}
|
||||
borderRadius="20px"
|
||||
padding="10px 1rem"
|
||||
onClick={handleAddList}
|
||||
>
|
||||
Import
|
||||
</ButtonPrimary>
|
||||
{addError ? (
|
||||
<TYPE.error title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
|
||||
{addError}
|
||||
</TYPE.error>
|
||||
) : null}
|
||||
</AutoColumn>
|
||||
{/* </Card> */}
|
||||
</PaddedColumn>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
108
src/components/SearchModal/ImportRow.tsx
Normal file
108
src/components/SearchModal/ImportRow.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import React, { CSSProperties } from 'react'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import { RowBetween, RowFixed, AutoRow } from 'components/Row'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { TYPE } from 'theme'
|
||||
import ListLogo from 'components/ListLogo'
|
||||
import { useActiveWeb3React } from 'hooks'
|
||||
import { useCombinedInactiveList } from 'state/lists/hooks'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import styled from 'styled-components'
|
||||
import { useIsUserAddedToken, useIsTokenActive } from 'hooks/Tokens'
|
||||
import { CheckCircle } from 'react-feather'
|
||||
|
||||
const TokenSection = styled.div`
|
||||
padding: 8px 20px;
|
||||
height: 56px;
|
||||
`
|
||||
|
||||
const CheckIcon = styled(CheckCircle)`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: 6px;
|
||||
stroke: ${({ theme }) => theme.green1};
|
||||
`
|
||||
|
||||
const NameOverflow = styled.div`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 140px;
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
export default function ImportRow({
|
||||
token,
|
||||
style,
|
||||
dim,
|
||||
showImportView,
|
||||
setImportToken
|
||||
}: {
|
||||
token: Token
|
||||
style?: CSSProperties
|
||||
dim?: boolean
|
||||
showImportView: () => void
|
||||
setImportToken: (token: Token) => void
|
||||
}) {
|
||||
// gloabls
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useTheme()
|
||||
|
||||
// check if token comes from list
|
||||
const inactiveTokenList = useCombinedInactiveList()
|
||||
const list = chainId && inactiveTokenList?.[chainId]?.[token.address]?.list
|
||||
|
||||
// check if already active on list or local storage tokens
|
||||
const isAdded = useIsUserAddedToken(token)
|
||||
const isActive = useIsTokenActive(token)
|
||||
|
||||
return (
|
||||
<TokenSection style={style}>
|
||||
<RowBetween>
|
||||
<AutoRow style={{ opacity: dim ? '0.6' : '1' }}>
|
||||
<CurrencyLogo currency={token} size={'24px'} />
|
||||
<AutoColumn gap="4px">
|
||||
<AutoRow>
|
||||
<TYPE.body ml="8px" fontWeight={500}>
|
||||
{token.symbol}
|
||||
</TYPE.body>
|
||||
<TYPE.darkGray ml="8px" fontWeight={300}>
|
||||
<NameOverflow title={token.name}>{token.name}</NameOverflow>
|
||||
</TYPE.darkGray>
|
||||
</AutoRow>
|
||||
{list && list.logoURI && (
|
||||
<RowFixed style={{ marginLeft: '8px' }}>
|
||||
<TYPE.small mr="4px" color={theme.text3}>
|
||||
via {list.name}
|
||||
</TYPE.small>
|
||||
<ListLogo logoURI={list.logoURI} size="12px" />
|
||||
</RowFixed>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
{!isActive && !isAdded ? (
|
||||
<ButtonPrimary
|
||||
width="fit-content"
|
||||
padding="6px 12px"
|
||||
fontWeight={500}
|
||||
fontSize="14px"
|
||||
onClick={() => {
|
||||
setImportToken && setImportToken(token)
|
||||
showImportView()
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</ButtonPrimary>
|
||||
) : (
|
||||
<RowFixed style={{ minWidth: 'fit-content' }}>
|
||||
<CheckIcon />
|
||||
<TYPE.main color={theme.green1}>Active</TYPE.main>
|
||||
</RowFixed>
|
||||
)}
|
||||
</RowBetween>
|
||||
</TokenSection>
|
||||
)
|
||||
}
|
146
src/components/SearchModal/ImportToken.tsx
Normal file
146
src/components/SearchModal/ImportToken.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Token, Currency } from '@uniswap/sdk'
|
||||
import styled from 'styled-components'
|
||||
import { TYPE, CloseIcon } from 'theme'
|
||||
import Card from 'components/Card'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { RowBetween, RowFixed, AutoRow } from 'components/Row'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { ArrowLeft, AlertTriangle } from 'react-feather'
|
||||
import { transparentize } from 'polished'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import { SectionBreak } from 'components/swap/styleds'
|
||||
import { useAddUserToken } from 'state/user/hooks'
|
||||
import { getEtherscanLink } from 'utils'
|
||||
import { useActiveWeb3React } from 'hooks'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { useCombinedInactiveList } from 'state/lists/hooks'
|
||||
import ListLogo from 'components/ListLogo'
|
||||
import { PaddedColumn, Checkbox } from './styleds'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const WarningWrapper = styled(Card)<{ highWarning: boolean }>`
|
||||
background-color: ${({ theme, highWarning }) =>
|
||||
highWarning ? transparentize(0.8, theme.red1) : transparentize(0.8, theme.yellow2)};
|
||||
width: fit-content;
|
||||
`
|
||||
|
||||
const AddressText = styled(TYPE.blue)`
|
||||
font-size: 12px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
font-size: 10px;
|
||||
`}
|
||||
`
|
||||
|
||||
interface ImportProps {
|
||||
token: Token
|
||||
onBack: () => void
|
||||
onDismiss: () => void
|
||||
handleCurrencySelect: (currency: Currency) => void
|
||||
}
|
||||
|
||||
export function ImportToken({ token, onBack, onDismiss, handleCurrencySelect }: ImportProps) {
|
||||
const theme = useTheme()
|
||||
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const [confirmed, setConfirmed] = useState(false)
|
||||
|
||||
const addToken = useAddUserToken()
|
||||
|
||||
// use for showing import source on inactive tokens
|
||||
const inactiveTokenList = useCombinedInactiveList()
|
||||
|
||||
const list = chainId && inactiveTokenList?.[chainId]?.[token.address]?.list
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn gap="14px" style={{ width: '100%', flex: '1 1' }}>
|
||||
<RowBetween>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} />
|
||||
<TYPE.mediumHeader>Import Token</TYPE.mediumHeader>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<SectionBreak />
|
||||
<PaddedColumn gap="md">
|
||||
<Card backgroundColor={theme.bg2}>
|
||||
<AutoColumn gap="10px">
|
||||
<AutoRow align="center">
|
||||
<CurrencyLogo currency={token} size={'24px'} />
|
||||
<TYPE.body ml="8px" mr="8px" fontWeight={500}>
|
||||
{token.symbol}
|
||||
</TYPE.body>
|
||||
<TYPE.darkGray fontWeight={300}>{token.name}</TYPE.darkGray>
|
||||
</AutoRow>
|
||||
{chainId && (
|
||||
<ExternalLink href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
<AddressText>{token.address}</AddressText>
|
||||
</ExternalLink>
|
||||
)}
|
||||
{list !== undefined ? (
|
||||
<RowFixed>
|
||||
{list.logoURI && <ListLogo logoURI={list.logoURI} size="12px" />}
|
||||
<TYPE.small ml="6px" color={theme.text3}>
|
||||
via {list.name}
|
||||
</TYPE.small>
|
||||
</RowFixed>
|
||||
) : (
|
||||
<WarningWrapper borderRadius="4px" padding="4px" highWarning={true}>
|
||||
<RowFixed>
|
||||
<AlertTriangle stroke={theme.red1} size="10px" />
|
||||
<TYPE.body color={theme.red1} ml="4px" fontSize="10px" fontWeight={500}>
|
||||
Unkown Source
|
||||
</TYPE.body>
|
||||
</RowFixed>
|
||||
</WarningWrapper>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Card>
|
||||
<Card style={{ backgroundColor: list ? transparentize(0.8, theme.yellow2) : transparentize(0.8, theme.red1) }}>
|
||||
<AutoColumn justify="center" style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
|
||||
<AlertTriangle stroke={list ? theme.yellow2 : theme.red1} size={32} />
|
||||
<TYPE.body fontWeight={600} fontSize={20} color={list ? theme.yellow2 : theme.red1}>
|
||||
Trade at your own risk!
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
|
||||
<AutoColumn style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
|
||||
<TYPE.body fontWeight={400} color={list ? theme.yellow2 : theme.red1}>
|
||||
Anyone can create a token, including creating fake versions of existing tokens that claim to represent
|
||||
projects.
|
||||
</TYPE.body>
|
||||
<TYPE.body fontWeight={600} color={list ? theme.yellow2 : theme.red1}>
|
||||
If you purchase this token, you may not be able to sell it back.
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
<AutoRow justify="center" style={{ cursor: 'pointer' }} onClick={() => setConfirmed(!confirmed)}>
|
||||
<Checkbox name="confirmed" type="checkbox" checked={confirmed} onChange={() => setConfirmed(!confirmed)} />
|
||||
<TYPE.body ml="10px" fontSize="16px" color={list ? theme.yellow2 : theme.red1} fontWeight={500}>
|
||||
I understand
|
||||
</TYPE.body>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
|
||||
<ButtonPrimary
|
||||
disabled={!confirmed}
|
||||
altDisabledStyle={true}
|
||||
borderRadius="20px"
|
||||
padding="10px 1rem"
|
||||
onClick={() => {
|
||||
addToken(token)
|
||||
handleCurrencySelect(token)
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</ButtonPrimary>
|
||||
</PaddedColumn>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
@ -1,378 +0,0 @@
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { usePopper } from 'react-popper'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
import { AppDispatch, AppState } from '../../state'
|
||||
import { acceptListUpdate, removeList, selectList } from '../../state/lists/actions'
|
||||
import { useSelectedListUrl } from '../../state/lists/hooks'
|
||||
import { CloseIcon, ExternalLink, LinkStyledButton, TYPE } from '../../theme'
|
||||
import listVersionLabel from '../../utils/listVersionLabel'
|
||||
import { parseENSAddress } from '../../utils/parseENSAddress'
|
||||
import uriToHttp from '../../utils/uriToHttp'
|
||||
import { ButtonOutlined, ButtonPrimary, ButtonSecondary } from '../Button'
|
||||
|
||||
import Column from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
|
||||
|
||||
const UnpaddedLinkStyledButton = styled(LinkStyledButton)`
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 100;
|
||||
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
color: ${({ theme }) => theme.text2};
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 8px;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
`
|
||||
|
||||
const StyledListUrlText = styled.div`
|
||||
max-width: 160px;
|
||||
opacity: 0.6;
|
||||
margin-right: 0.5rem;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
function ListOrigin({ listUrl }: { listUrl: string }) {
|
||||
const ensName = useMemo(() => parseENSAddress(listUrl)?.ensName, [listUrl])
|
||||
const host = useMemo(() => {
|
||||
if (ensName) return undefined
|
||||
const lowerListUrl = listUrl.toLowerCase()
|
||||
if (lowerListUrl.startsWith('ipfs://') || lowerListUrl.startsWith('ipns://')) {
|
||||
return listUrl
|
||||
}
|
||||
try {
|
||||
const url = new URL(listUrl)
|
||||
return url.host
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}, [listUrl, ensName])
|
||||
return <>{ensName ?? host}</>
|
||||
}
|
||||
|
||||
function listUrlRowHTMLId(listUrl: string) {
|
||||
return `list-row-${listUrl.replace(/\./g, '-')}`
|
||||
}
|
||||
|
||||
const ListRow = memo(function ListRow({ listUrl, onBack }: { listUrl: string; onBack: () => void }) {
|
||||
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const selectedListUrl = useSelectedListUrl()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]
|
||||
|
||||
const isSelected = listUrl === selectedListUrl
|
||||
|
||||
const [open, toggle] = useToggle(false)
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>()
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>()
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'auto',
|
||||
strategy: 'fixed',
|
||||
modifiers: [{ name: 'offset', options: { offset: [8, 8] } }]
|
||||
})
|
||||
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
const selectThisList = useCallback(() => {
|
||||
if (isSelected) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Select List',
|
||||
label: listUrl
|
||||
})
|
||||
|
||||
dispatch(selectList(listUrl))
|
||||
onBack()
|
||||
}, [dispatch, isSelected, listUrl, onBack])
|
||||
|
||||
const handleAcceptListUpdate = useCallback(() => {
|
||||
if (!pending) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Update List from List Select',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
}, [dispatch, listUrl, pending])
|
||||
|
||||
const handleRemoveList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Start Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
if (window.prompt(`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Confirm Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(removeList(listUrl))
|
||||
}
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
if (!list) return null
|
||||
|
||||
return (
|
||||
<Row key={listUrl} align="center" padding="16px" id={listUrlRowHTMLId(listUrl)}>
|
||||
{list.logoURI ? (
|
||||
<ListLogo style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
|
||||
) : (
|
||||
<div style={{ width: '24px', height: '24px', marginRight: '1rem' }} />
|
||||
)}
|
||||
<Column style={{ flex: '1' }}>
|
||||
<Row>
|
||||
<Text
|
||||
fontWeight={isSelected ? 500 : 400}
|
||||
fontSize={16}
|
||||
style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{list.name}
|
||||
</Text>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
marginTop: '4px'
|
||||
}}
|
||||
>
|
||||
<StyledListUrlText title={listUrl}>
|
||||
<ListOrigin listUrl={listUrl} />
|
||||
</StyledListUrlText>
|
||||
</Row>
|
||||
</Column>
|
||||
<StyledMenu ref={node as any}>
|
||||
<ButtonOutlined
|
||||
style={{
|
||||
width: '2rem',
|
||||
padding: '.8rem .35rem',
|
||||
borderRadius: '12px',
|
||||
fontSize: '14px',
|
||||
marginRight: '0.5rem'
|
||||
}}
|
||||
onClick={toggle}
|
||||
ref={setReferenceElement}
|
||||
>
|
||||
<DropDown />
|
||||
</ButtonOutlined>
|
||||
|
||||
{open && (
|
||||
<PopoverContainer show={true} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
|
||||
<div>{list && listVersionLabel(list.version)}</div>
|
||||
<SeparatorDark />
|
||||
<ExternalLink href={`https://tokenlists.org/token-list?url=${listUrl}`}>View list</ExternalLink>
|
||||
<UnpaddedLinkStyledButton onClick={handleRemoveList} disabled={Object.keys(listsByUrl).length === 1}>
|
||||
Remove list
|
||||
</UnpaddedLinkStyledButton>
|
||||
{pending && (
|
||||
<UnpaddedLinkStyledButton onClick={handleAcceptListUpdate}>Update list</UnpaddedLinkStyledButton>
|
||||
)}
|
||||
</PopoverContainer>
|
||||
)}
|
||||
</StyledMenu>
|
||||
{isSelected ? (
|
||||
<ButtonPrimary
|
||||
disabled={true}
|
||||
className="select-button"
|
||||
style={{ width: '5rem', minWidth: '5rem', padding: '0.5rem .35rem', borderRadius: '12px', fontSize: '14px' }}
|
||||
>
|
||||
Selected
|
||||
</ButtonPrimary>
|
||||
) : (
|
||||
<>
|
||||
<ButtonPrimary
|
||||
className="select-button"
|
||||
style={{
|
||||
width: '5rem',
|
||||
minWidth: '4.5rem',
|
||||
padding: '0.5rem .35rem',
|
||||
borderRadius: '12px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
onClick={selectThisList}
|
||||
>
|
||||
Select
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
|
||||
const AddListButton = styled(ButtonSecondary)`
|
||||
max-width: 4rem;
|
||||
margin-left: 1rem;
|
||||
border-radius: 12px;
|
||||
padding: 10px 18px;
|
||||
`
|
||||
|
||||
const ListContainer = styled.div`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
export function ListSelect({ onDismiss, onBack }: { onDismiss: () => void; onBack: () => void }) {
|
||||
const [listUrlInput, setListUrlInput] = useState<string>('')
|
||||
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const adding = Boolean(lists[listUrlInput]?.loadingRequestId)
|
||||
const [addError, setAddError] = useState<string | null>(null)
|
||||
|
||||
const handleInput = useCallback(e => {
|
||||
setListUrlInput(e.target.value)
|
||||
setAddError(null)
|
||||
}, [])
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
const handleAddList = useCallback(() => {
|
||||
if (adding) return
|
||||
setAddError(null)
|
||||
fetchList(listUrlInput)
|
||||
.then(() => {
|
||||
setListUrlInput('')
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List',
|
||||
label: listUrlInput
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List Failed',
|
||||
label: listUrlInput
|
||||
})
|
||||
setAddError(error.message)
|
||||
dispatch(removeList(listUrlInput))
|
||||
})
|
||||
}, [adding, dispatch, fetchList, listUrlInput])
|
||||
|
||||
const validUrl: boolean = useMemo(() => {
|
||||
return uriToHttp(listUrlInput).length > 0 || Boolean(parseENSAddress(listUrlInput))
|
||||
}, [listUrlInput])
|
||||
|
||||
const handleEnterKey = useCallback(
|
||||
e => {
|
||||
if (validUrl && e.key === 'Enter') {
|
||||
handleAddList()
|
||||
}
|
||||
},
|
||||
[handleAddList, validUrl]
|
||||
)
|
||||
|
||||
const sortedLists = useMemo(() => {
|
||||
const listUrls = Object.keys(lists)
|
||||
return listUrls
|
||||
.filter(listUrl => {
|
||||
return Boolean(lists[listUrl].current)
|
||||
})
|
||||
.sort((u1, u2) => {
|
||||
const { current: l1 } = lists[u1]
|
||||
const { current: l2 } = lists[u2]
|
||||
if (l1 && l2) {
|
||||
return l1.name.toLowerCase() < l2.name.toLowerCase()
|
||||
? -1
|
||||
: l1.name.toLowerCase() === l2.name.toLowerCase()
|
||||
? 0
|
||||
: 1
|
||||
}
|
||||
if (l1) return -1
|
||||
if (l2) return 1
|
||||
return 0
|
||||
})
|
||||
}, [lists])
|
||||
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn>
|
||||
<RowBetween>
|
||||
<div>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} />
|
||||
</div>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Manage Lists
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
|
||||
<PaddedColumn gap="14px">
|
||||
<Text fontWeight={600}>
|
||||
Add a list{' '}
|
||||
<QuestionHelper text="Token lists are an open specification for lists of ERC20 tokens. You can use any token list by entering its URL below. Beware that third party token lists can contain fake or malicious ERC20 tokens." />
|
||||
</Text>
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="list-add-input"
|
||||
placeholder="https:// or ipfs:// or ENS name"
|
||||
value={listUrlInput}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnterKey}
|
||||
style={{ height: '2.75rem', borderRadius: 12, padding: '12px' }}
|
||||
/>
|
||||
<AddListButton onClick={handleAddList} disabled={!validUrl}>
|
||||
Add
|
||||
</AddListButton>
|
||||
</Row>
|
||||
{addError ? (
|
||||
<TYPE.error title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
|
||||
{addError}
|
||||
</TYPE.error>
|
||||
) : null}
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
|
||||
<ListContainer>
|
||||
{sortedLists.map(listUrl => (
|
||||
<ListRow key={listUrl} listUrl={listUrl} onBack={onBack} />
|
||||
))}
|
||||
</ListContainer>
|
||||
<Separator />
|
||||
|
||||
<div style={{ padding: '16px', textAlign: 'center' }}>
|
||||
<ExternalLink href="https://tokenlists.org">Browse lists</ExternalLink>
|
||||
</div>
|
||||
</Column>
|
||||
)
|
||||
}
|
89
src/components/SearchModal/Manage.tsx
Normal file
89
src/components/SearchModal/Manage.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { useState } from 'react'
|
||||
import { PaddedColumn, Separator } from './styleds'
|
||||
import { RowBetween } from 'components/Row'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { CloseIcon } from 'theme'
|
||||
import styled from 'styled-components'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import { ManageLists } from './ManageLists'
|
||||
import ManageTokens from './ManageTokens'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-bottom: 80px;
|
||||
`
|
||||
|
||||
const ToggleWrapper = styled(RowBetween)`
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
border-radius: 12px;
|
||||
padding: 6px;
|
||||
`
|
||||
|
||||
const ToggleOption = styled.div<{ active?: boolean }>`
|
||||
width: 48%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
background-color: ${({ theme, active }) => (active ? theme.bg1 : theme.bg3)};
|
||||
color: ${({ theme, active }) => (active ? theme.text1 : theme.text2)};
|
||||
user-select: none;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
`
|
||||
|
||||
export default function Manage({
|
||||
onDismiss,
|
||||
setModalView,
|
||||
setImportList,
|
||||
setImportToken,
|
||||
setListUrl
|
||||
}: {
|
||||
onDismiss: () => void
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
setImportToken: (token: Token) => void
|
||||
setImportList: (list: TokenList) => void
|
||||
setListUrl: (url: string) => void
|
||||
}) {
|
||||
// toggle between tokens and lists
|
||||
const [showLists, setShowLists] = useState(true)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn>
|
||||
<RowBetween>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={() => setModalView(CurrencyModalView.search)} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Manage
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<Separator />
|
||||
<PaddedColumn style={{ paddingBottom: 0 }}>
|
||||
<ToggleWrapper>
|
||||
<ToggleOption onClick={() => setShowLists(!showLists)} active={showLists}>
|
||||
Lists
|
||||
</ToggleOption>
|
||||
<ToggleOption onClick={() => setShowLists(!showLists)} active={!showLists}>
|
||||
Tokens
|
||||
</ToggleOption>
|
||||
</ToggleWrapper>
|
||||
</PaddedColumn>
|
||||
{showLists ? (
|
||||
<ManageLists setModalView={setModalView} setImportList={setImportList} setListUrl={setListUrl} />
|
||||
) : (
|
||||
<ManageTokens setModalView={setModalView} setImportToken={setImportToken} />
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
377
src/components/SearchModal/ManageLists.tsx
Normal file
377
src/components/SearchModal/ManageLists.tsx
Normal file
@ -0,0 +1,377 @@
|
||||
import React, { memo, useCallback, useMemo, useRef, useState, useEffect } from 'react'
|
||||
import { Settings, CheckCircle } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { usePopper } from 'react-popper'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
import { AppDispatch, AppState } from '../../state'
|
||||
import { acceptListUpdate, removeList, disableList, enableList } from '../../state/lists/actions'
|
||||
import { useIsListActive, useAllLists, useActiveListUrls } from '../../state/lists/hooks'
|
||||
import { ExternalLink, LinkStyledButton, TYPE, IconWrapper } from '../../theme'
|
||||
import listVersionLabel from '../../utils/listVersionLabel'
|
||||
import { parseENSAddress } from '../../utils/parseENSAddress'
|
||||
import uriToHttp from '../../utils/uriToHttp'
|
||||
import { ButtonEmpty, ButtonPrimary } from '../Button'
|
||||
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import Row, { RowFixed, RowBetween } from '../Row'
|
||||
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
|
||||
import { useListColor } from 'hooks/useColor'
|
||||
import useTheme from '../../hooks/useTheme'
|
||||
import ListToggle from '../Toggle/ListToggle'
|
||||
import Card from 'components/Card'
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
|
||||
|
||||
const Wrapper = styled(Column)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const UnpaddedLinkStyledButton = styled(LinkStyledButton)`
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 100;
|
||||
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
color: ${({ theme }) => theme.text2};
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 8px;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
`
|
||||
|
||||
const StyledTitleText = styled.div<{ active: boolean }>`
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
color: ${({ theme, active }) => (active ? theme.white : theme.text2)};
|
||||
`
|
||||
|
||||
const StyledListUrlText = styled(TYPE.main)<{ active: boolean }>`
|
||||
font-size: 12px;
|
||||
color: ${({ theme, active }) => (active ? theme.white : theme.text2)};
|
||||
`
|
||||
|
||||
const RowWrapper = styled(Row)<{ bgColor: string; active: boolean }>`
|
||||
background-color: ${({ bgColor, active, theme }) => (active ? bgColor ?? 'transparent' : theme.bg2)};
|
||||
transition: 200ms;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
function listUrlRowHTMLId(listUrl: string) {
|
||||
return `list-row-${listUrl.replace(/\./g, '-')}`
|
||||
}
|
||||
|
||||
const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
|
||||
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]
|
||||
|
||||
const theme = useTheme()
|
||||
const listColor = useListColor(list?.logoURI)
|
||||
const isActive = useIsListActive(listUrl)
|
||||
|
||||
const [open, toggle] = useToggle(false)
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>()
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>()
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'auto',
|
||||
strategy: 'fixed',
|
||||
modifiers: [{ name: 'offset', options: { offset: [8, 8] } }]
|
||||
})
|
||||
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
const handleAcceptListUpdate = useCallback(() => {
|
||||
if (!pending) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Update List from List Select',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
}, [dispatch, listUrl, pending])
|
||||
|
||||
const handleRemoveList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Start Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
if (window.prompt(`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Confirm Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(removeList(listUrl))
|
||||
}
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
const handleEnableList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Enable List',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(enableList(listUrl))
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
const handleDisableList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Disable List',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(disableList(listUrl))
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
if (!list) return null
|
||||
|
||||
return (
|
||||
<RowWrapper active={isActive} bgColor={listColor} key={listUrl} id={listUrlRowHTMLId(listUrl)}>
|
||||
{list.logoURI ? (
|
||||
<ListLogo size="40px" style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
|
||||
) : (
|
||||
<div style={{ width: '24px', height: '24px', marginRight: '1rem' }} />
|
||||
)}
|
||||
<Column style={{ flex: '1' }}>
|
||||
<Row>
|
||||
<StyledTitleText active={isActive}>{list.name}</StyledTitleText>
|
||||
</Row>
|
||||
<RowFixed mt="4px">
|
||||
<StyledListUrlText active={isActive} mr="6px">
|
||||
{list.tokens.length} tokens
|
||||
</StyledListUrlText>
|
||||
<StyledMenu ref={node as any}>
|
||||
<ButtonEmpty onClick={toggle} ref={setReferenceElement} padding="0">
|
||||
<Settings stroke={isActive ? theme.bg1 : theme.text1} size={12} />
|
||||
</ButtonEmpty>
|
||||
{open && (
|
||||
<PopoverContainer show={true} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
|
||||
<div>{list && listVersionLabel(list.version)}</div>
|
||||
<SeparatorDark />
|
||||
<ExternalLink href={`https://tokenlists.org/token-list?url=${listUrl}`}>View list</ExternalLink>
|
||||
<UnpaddedLinkStyledButton onClick={handleRemoveList} disabled={Object.keys(listsByUrl).length === 1}>
|
||||
Remove list
|
||||
</UnpaddedLinkStyledButton>
|
||||
{pending && (
|
||||
<UnpaddedLinkStyledButton onClick={handleAcceptListUpdate}>Update list</UnpaddedLinkStyledButton>
|
||||
)}
|
||||
</PopoverContainer>
|
||||
)}
|
||||
</StyledMenu>
|
||||
</RowFixed>
|
||||
</Column>
|
||||
<ListToggle
|
||||
isActive={isActive}
|
||||
bgColor={listColor}
|
||||
toggle={() => {
|
||||
isActive ? handleDisableList() : handleEnableList()
|
||||
}}
|
||||
/>
|
||||
</RowWrapper>
|
||||
)
|
||||
})
|
||||
|
||||
const ListContainer = styled.div`
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding-bottom: 80px;
|
||||
`
|
||||
|
||||
export function ManageLists({
|
||||
setModalView,
|
||||
setImportList,
|
||||
setListUrl
|
||||
}: {
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
setImportList: (list: TokenList) => void
|
||||
setListUrl: (url: string) => void
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
|
||||
const [listUrlInput, setListUrlInput] = useState<string>('')
|
||||
|
||||
const lists = useAllLists()
|
||||
|
||||
// sort by active but only if not visible
|
||||
const activeListUrls = useActiveListUrls()
|
||||
const [activeCopy, setActiveCopy] = useState<string[] | undefined>()
|
||||
useEffect(() => {
|
||||
if (!activeCopy && activeListUrls) {
|
||||
setActiveCopy(activeListUrls)
|
||||
}
|
||||
}, [activeCopy, activeListUrls])
|
||||
|
||||
const handleInput = useCallback(e => {
|
||||
setListUrlInput(e.target.value)
|
||||
}, [])
|
||||
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
const validUrl: boolean = useMemo(() => {
|
||||
return uriToHttp(listUrlInput).length > 0 || Boolean(parseENSAddress(listUrlInput))
|
||||
}, [listUrlInput])
|
||||
|
||||
const sortedLists = useMemo(() => {
|
||||
const listUrls = Object.keys(lists)
|
||||
return listUrls
|
||||
.filter(listUrl => {
|
||||
// only show loaded lists, hide unsupported lists
|
||||
return Boolean(lists[listUrl].current) && !Boolean(UNSUPPORTED_LIST_URLS.includes(listUrl))
|
||||
})
|
||||
.sort((u1, u2) => {
|
||||
const { current: l1 } = lists[u1]
|
||||
const { current: l2 } = lists[u2]
|
||||
|
||||
// first filter on active lists
|
||||
if (activeCopy?.includes(u1) && !activeCopy?.includes(u2)) {
|
||||
return -1
|
||||
}
|
||||
if (!activeCopy?.includes(u1) && activeCopy?.includes(u2)) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (l1 && l2) {
|
||||
return l1.name.toLowerCase() < l2.name.toLowerCase()
|
||||
? -1
|
||||
: l1.name.toLowerCase() === l2.name.toLowerCase()
|
||||
? 0
|
||||
: 1
|
||||
}
|
||||
if (l1) return -1
|
||||
if (l2) return 1
|
||||
return 0
|
||||
})
|
||||
}, [lists, activeCopy])
|
||||
|
||||
// temporary fetched list for import flow
|
||||
const [tempList, setTempList] = useState<TokenList>()
|
||||
const [addError, setAddError] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchTempList() {
|
||||
fetchList(listUrlInput, false)
|
||||
.then(list => setTempList(list))
|
||||
.catch(() => setAddError('Error importing list'))
|
||||
}
|
||||
// if valid url, fetch details for card
|
||||
if (validUrl) {
|
||||
fetchTempList()
|
||||
} else {
|
||||
setTempList(undefined)
|
||||
listUrlInput !== '' && setAddError('Enter valid list location')
|
||||
}
|
||||
|
||||
// reset error
|
||||
if (listUrlInput === '') {
|
||||
setAddError(undefined)
|
||||
}
|
||||
}, [fetchList, listUrlInput, validUrl])
|
||||
|
||||
// check if list is already imported
|
||||
const isImported = Object.keys(lists).includes(listUrlInput)
|
||||
|
||||
// set list values and have parent modal switch to import list view
|
||||
const handleImport = useCallback(() => {
|
||||
if (!tempList) return
|
||||
setImportList(tempList)
|
||||
setModalView(CurrencyModalView.importList)
|
||||
setListUrl(listUrlInput)
|
||||
}, [listUrlInput, setImportList, setListUrl, setModalView, tempList])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn gap="14px">
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="list-add-input"
|
||||
placeholder="https:// or ipfs:// or ENS name"
|
||||
value={listUrlInput}
|
||||
onChange={handleInput}
|
||||
/>
|
||||
</Row>
|
||||
{addError ? (
|
||||
<TYPE.error title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
|
||||
{addError}
|
||||
</TYPE.error>
|
||||
) : null}
|
||||
</PaddedColumn>
|
||||
{tempList && (
|
||||
<PaddedColumn style={{ paddingTop: 0 }}>
|
||||
<Card backgroundColor={theme.bg2} padding="12px 20px">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
{tempList.logoURI && <ListLogo logoURI={tempList.logoURI} size="40px" />}
|
||||
<AutoColumn gap="4px" style={{ marginLeft: '20px' }}>
|
||||
<TYPE.body fontWeight={600}>{tempList.name}</TYPE.body>
|
||||
<TYPE.main fontSize={'12px'}>{tempList.tokens.length} tokens</TYPE.main>
|
||||
</AutoColumn>
|
||||
</RowFixed>
|
||||
{isImported ? (
|
||||
<RowFixed>
|
||||
<IconWrapper stroke={theme.text2} size="16px" marginRight={'10px'}>
|
||||
<CheckCircle />
|
||||
</IconWrapper>
|
||||
<TYPE.body color={theme.text2}>Loaded</TYPE.body>
|
||||
</RowFixed>
|
||||
) : (
|
||||
<ButtonPrimary
|
||||
style={{ fontSize: '14px' }}
|
||||
padding="6px 8px"
|
||||
width="fit-content"
|
||||
onClick={handleImport}
|
||||
>
|
||||
Import
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</RowBetween>
|
||||
</Card>
|
||||
</PaddedColumn>
|
||||
)}
|
||||
<Separator />
|
||||
<ListContainer>
|
||||
<AutoColumn gap="md">
|
||||
{sortedLists.map(listUrl => (
|
||||
<ListRow key={listUrl} listUrl={listUrl} />
|
||||
))}
|
||||
</AutoColumn>
|
||||
</ListContainer>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
143
src/components/SearchModal/ManageTokens.tsx
Normal file
143
src/components/SearchModal/ManageTokens.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import React, { useRef, RefObject, useCallback, useState, useMemo } from 'react'
|
||||
import Column from 'components/Column'
|
||||
import { PaddedColumn, Separator, SearchInput } from './styleds'
|
||||
import Row, { RowBetween, RowFixed } from 'components/Row'
|
||||
import { TYPE, ExternalLinkIcon, TrashIcon, ButtonText, ExternalLink } from 'theme'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import styled from 'styled-components'
|
||||
import { useUserAddedTokens, useRemoveUserAddedToken } from 'state/user/hooks'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { getEtherscanLink, isAddress } from 'utils'
|
||||
import { useActiveWeb3React } from 'hooks'
|
||||
import Card from 'components/Card'
|
||||
import ImportRow from './ImportRow'
|
||||
import useTheme from '../../hooks/useTheme'
|
||||
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
height: calc(100% - 60px);
|
||||
position: relative;
|
||||
padding-bottom: 60px;
|
||||
`
|
||||
|
||||
const Footer = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-radius: 20px;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top: 1px solid ${({ theme }) => theme.bg3};
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
export default function ManageTokens({
|
||||
setModalView,
|
||||
setImportToken
|
||||
}: {
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
setImportToken: (token: Token) => void
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const theme = useTheme()
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
const handleInput = useCallback(event => {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}, [])
|
||||
|
||||
// if they input an address, use it
|
||||
const isAddressSearch = isAddress(searchQuery)
|
||||
const searchToken = useToken(searchQuery)
|
||||
|
||||
// all tokens for local lisr
|
||||
const userAddedTokens: Token[] = useUserAddedTokens()
|
||||
const removeToken = useRemoveUserAddedToken()
|
||||
|
||||
const handleRemoveAll = useCallback(() => {
|
||||
if (chainId && userAddedTokens) {
|
||||
userAddedTokens.map(token => {
|
||||
return removeToken(chainId, token.address)
|
||||
})
|
||||
}
|
||||
}, [removeToken, userAddedTokens, chainId])
|
||||
|
||||
const tokenList = useMemo(() => {
|
||||
return (
|
||||
chainId &&
|
||||
userAddedTokens.map(token => (
|
||||
<RowBetween key={token.address} width="100%">
|
||||
<RowFixed>
|
||||
<CurrencyLogo currency={token} size={'20px'} />
|
||||
<ExternalLink href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
<TYPE.main ml={'10px'} fontWeight={600}>
|
||||
{token.symbol}
|
||||
</TYPE.main>
|
||||
</ExternalLink>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<TrashIcon onClick={() => removeToken(chainId, token.address)} />
|
||||
<ExternalLinkIcon href={getEtherscanLink(chainId, token.address, 'address')} />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
))
|
||||
)
|
||||
}, [userAddedTokens, chainId, removeToken])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn gap="14px">
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={'0x0000'}
|
||||
value={searchQuery}
|
||||
autoComplete="off"
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
onChange={handleInput}
|
||||
/>
|
||||
</Row>
|
||||
{searchQuery !== '' && !isAddressSearch && <TYPE.error error={true}>Enter valid token address</TYPE.error>}
|
||||
{searchToken && (
|
||||
<Card backgroundColor={theme.bg2} padding="10px 0">
|
||||
<ImportRow
|
||||
token={searchToken}
|
||||
showImportView={() => setModalView(CurrencyModalView.importToken)}
|
||||
setImportToken={setImportToken}
|
||||
style={{ height: 'fit-content' }}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</PaddedColumn>
|
||||
<Separator />
|
||||
<PaddedColumn gap="lg">
|
||||
<RowBetween>
|
||||
<TYPE.main fontWeight={600}>
|
||||
{userAddedTokens?.length} Custom {userAddedTokens.length === 1 ? 'Token' : 'Tokens'}
|
||||
</TYPE.main>
|
||||
{userAddedTokens.length > 0 && (
|
||||
<ButtonText onClick={handleRemoveAll}>
|
||||
<TYPE.blue>Clear all</TYPE.blue>
|
||||
</ButtonText>
|
||||
)}
|
||||
</RowBetween>
|
||||
{tokenList}
|
||||
</PaddedColumn>
|
||||
</Column>
|
||||
<Footer>
|
||||
<TYPE.darkGray>Tip: Custom tokens are stored locally in your browser</TYPE.darkGray>
|
||||
</Footer>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
@ -30,7 +30,15 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
|
||||
|
||||
return tokens.filter(token => {
|
||||
const { symbol, name } = token
|
||||
|
||||
return (symbol && matchesSearch(symbol)) || (name && matchesSearch(name))
|
||||
})
|
||||
// .sort((t0: Token, t1: Token) => {
|
||||
// if (t0.symbol && matchesSearch(t0.symbol) && t1.symbol && !matchesSearch(t1.symbol)) {
|
||||
// return -1
|
||||
// }
|
||||
// if (t0.symbol && !matchesSearch(t0.symbol) && t1.symbol && matchesSearch(t1.symbol)) {
|
||||
// return 1
|
||||
// }
|
||||
// return 0
|
||||
// })
|
||||
}
|
||||
|
@ -11,15 +11,53 @@ export const ModalInfo = styled.div`
|
||||
flex: 1;
|
||||
user-select: none;
|
||||
`
|
||||
export const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
`
|
||||
|
||||
export const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 100;
|
||||
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
color: ${({ theme }) => theme.text2};
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 8px;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
top: 80px;
|
||||
`
|
||||
|
||||
export const TextDot = styled.div`
|
||||
height: 3px;
|
||||
width: 3px;
|
||||
background-color: ${({ theme }) => theme.text2};
|
||||
border-radius: 50%;
|
||||
`
|
||||
|
||||
export const FadedSpan = styled(RowFixed)`
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
font-size: 14px;
|
||||
`
|
||||
export const Checkbox = styled.input`
|
||||
border: 1px solid ${({ theme }) => theme.red3};
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
export const PaddedColumn = styled(AutoColumn)`
|
||||
padding: 20px;
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
export const MenuItem = styled(RowBetween)`
|
||||
|
@ -6,7 +6,6 @@ import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import { ApplicationModal } from '../../state/application/actions'
|
||||
import { useModalOpen, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import {
|
||||
useDarkModeManager,
|
||||
useExpertModeManager,
|
||||
useUserTransactionTTL,
|
||||
useUserSlippageTolerance,
|
||||
@ -26,7 +25,7 @@ const StyledMenuIcon = styled(Settings)`
|
||||
width: 20px;
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
stroke: ${({ theme }) => theme.text3};
|
||||
}
|
||||
`
|
||||
|
||||
@ -51,7 +50,7 @@ const StyledMenuButton = styled.button`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 35px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
/* background-color: ${({ theme }) => theme.bg3}; */
|
||||
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
@ -60,7 +59,7 @@ const StyledMenuButton = styled.button`
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
/* background-color: ${({ theme }) => theme.bg4}; */
|
||||
}
|
||||
|
||||
svg {
|
||||
@ -94,18 +93,12 @@ const MenuFlyout = styled.span`
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
position: absolute;
|
||||
top: 4rem;
|
||||
top: 2rem;
|
||||
right: 0rem;
|
||||
z-index: 100;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
min-width: 18.125rem;
|
||||
right: -46px;
|
||||
`};
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
min-width: 18.125rem;
|
||||
top: -22rem;
|
||||
`};
|
||||
`
|
||||
|
||||
@ -138,8 +131,6 @@ export default function SettingsTab() {
|
||||
|
||||
const [singleHopOnly, setSingleHopOnly] = useUserSingleHopOnly()
|
||||
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
// show confirmation view before turning on
|
||||
const [showConfirmation, setShowConfirmation] = useState(false)
|
||||
|
||||
@ -246,14 +237,6 @@ export default function SettingsTab() {
|
||||
toggle={() => (singleHopOnly ? setSingleHopOnly(false) : setSingleHopOnly(true))}
|
||||
/>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Toggle Dark Mode
|
||||
</TYPE.black>
|
||||
</RowFixed>
|
||||
<Toggle isActive={darkMode} toggle={toggleDarkMode} />
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</MenuFlyout>
|
||||
)}
|
||||
|
56
src/components/Toggle/ListToggle.tsx
Normal file
56
src/components/Toggle/ListToggle.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0.4rem 0.4rem;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string }>`
|
||||
border-radius: 50%;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-color: ${({ isActive, bgColor, theme }) => (isActive ? bgColor : theme.bg4)};
|
||||
:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`
|
||||
|
||||
const StatusText = styled(TYPE.main)<{ isActive?: boolean }>`
|
||||
margin: 0 10px;
|
||||
width: 24px;
|
||||
color: ${({ theme, isActive }) => (isActive ? theme.text1 : theme.text3)};
|
||||
`
|
||||
|
||||
export interface ToggleProps {
|
||||
id?: string
|
||||
isActive: boolean
|
||||
bgColor: string
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function ListToggle({ id, isActive, bgColor, toggle }: ToggleProps) {
|
||||
return (
|
||||
<Wrapper id={id} isActive={isActive} onClick={toggle}>
|
||||
{isActive && (
|
||||
<StatusText fontWeight="600" margin="0 6px" isActive={true}>
|
||||
ON
|
||||
</StatusText>
|
||||
)}
|
||||
<ToggleElement isActive={isActive} bgColor={bgColor} />
|
||||
{!isActive && (
|
||||
<StatusText fontWeight="600" margin="0 6px" isActive={false}>
|
||||
OFF
|
||||
</StatusText>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
@ -26,14 +26,12 @@ const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
|
||||
const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
/* border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)}; */
|
||||
background: ${({ theme }) => theme.bg3};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
/* background-color: transparent; */
|
||||
`
|
||||
|
||||
export interface ToggleProps {
|
||||
|
@ -113,7 +113,7 @@ export default function TokenWarningModal({
|
||||
when interacting with arbitrary ERC20 tokens.
|
||||
</TYPE.body>
|
||||
<TYPE.body color={'red2'}>
|
||||
If you purchase an arbitrary token, <strong>you may be unable to sell it back.</strong>
|
||||
If you purchase an arbitrary token, <strong>you may not be able to sell it back.</strong>
|
||||
</TYPE.body>
|
||||
{tokens.map(token => {
|
||||
return <TokenWarningCard key={token.address} token={token} />
|
||||
|
@ -9,7 +9,6 @@ import { AutoColumn } from '../Column'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import { SectionBreak } from './styleds'
|
||||
import SwapRoute from './SwapRoute'
|
||||
|
||||
const InfoLink = styled(ExternalLink)`
|
||||
@ -30,7 +29,7 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoColumn style={{ padding: '0 20px' }}>
|
||||
<AutoColumn style={{ padding: '0 16px' }}>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
@ -86,29 +85,33 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
|
||||
const showRoute = Boolean(trade && trade.route.path.length > 2)
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<AutoColumn gap="0px">
|
||||
{trade && (
|
||||
<>
|
||||
<TradeSummary trade={trade} allowedSlippage={allowedSlippage} />
|
||||
{showRoute && (
|
||||
<>
|
||||
<SectionBreak />
|
||||
<AutoColumn style={{ padding: '0 24px' }}>
|
||||
<RowFixed>
|
||||
<RowBetween style={{ padding: '0 16px' }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Route
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
|
||||
</RowFixed>
|
||||
</span>
|
||||
<SwapRoute trade={trade} />
|
||||
</AutoColumn>
|
||||
</RowBetween>
|
||||
</>
|
||||
)}
|
||||
<AutoColumn style={{ padding: '0 24px' }}>
|
||||
<InfoLink href={'https://uniswap.info/pair/' + trade.route.pairs[0].liquidityToken.address} target="_blank">
|
||||
View pair analytics ↗
|
||||
</InfoLink>
|
||||
</AutoColumn>
|
||||
{!showRoute && (
|
||||
<AutoColumn style={{ padding: '12px 16px 0 16px' }}>
|
||||
<InfoLink
|
||||
href={'https://uniswap.info/pair/' + trade.route.pairs[0].liquidityToken.address}
|
||||
target="_blank"
|
||||
>
|
||||
View pair analytics ↗
|
||||
</InfoLink>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
|
@ -5,7 +5,7 @@ import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDet
|
||||
|
||||
const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
|
||||
padding-top: calc(16px + 2rem);
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
|
24
src/components/swap/SwapHeader.tsx
Normal file
24
src/components/swap/SwapHeader.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Settings from '../Settings'
|
||||
import { RowBetween } from '../Row'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
const StyledSwapHeader = styled.div`
|
||||
padding: 12px 1rem 0px 1.5rem;
|
||||
margin-bottom: -4px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
`
|
||||
|
||||
export default function SwapHeader() {
|
||||
return (
|
||||
<StyledSwapHeader>
|
||||
<RowBetween>
|
||||
<TYPE.black fontWeight={500}>Swap</TYPE.black>
|
||||
<Settings />
|
||||
</RowBetween>
|
||||
</StyledSwapHeader>
|
||||
)
|
||||
}
|
@ -4,32 +4,23 @@ import { ChevronRight } from 'react-feather'
|
||||
import { Flex } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { TYPE } from '../../theme'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { unwrappedToken } from 'utils/wrappedCurrency'
|
||||
|
||||
export default memo(function SwapRoute({ trade }: { trade: Trade }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<Flex
|
||||
px="1rem"
|
||||
py="0.5rem"
|
||||
my="0.5rem"
|
||||
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
|
||||
flexWrap="wrap"
|
||||
width="100%"
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
>
|
||||
<Flex flexWrap="wrap" width="100%" justifyContent="flex-end" alignItems="center">
|
||||
{trade.route.path.map((token, i, path) => {
|
||||
const isLastItem: boolean = i === path.length - 1
|
||||
const currency = unwrappedToken(token)
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<Flex my="0.5rem" alignItems="center" style={{ flexShrink: 0 }}>
|
||||
<CurrencyLogo currency={token} size="1.5rem" />
|
||||
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
|
||||
{token.symbol}
|
||||
<Flex alignItems="end">
|
||||
<TYPE.black fontSize={14} color={theme.text1} ml="0.125rem" mr="0.125rem">
|
||||
{currency.symbol}
|
||||
</TYPE.black>
|
||||
</Flex>
|
||||
{isLastItem ? null : <ChevronRight color={theme.text2} />}
|
||||
{isLastItem ? null : <ChevronRight size={12} color={theme.text2} />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
|
105
src/components/swap/UnsupportedCurrencyFooter.tsx
Normal file
105
src/components/swap/UnsupportedCurrencyFooter.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { TYPE, CloseIcon, ExternalLink } from 'theme'
|
||||
import { ButtonEmpty } from 'components/Button'
|
||||
import Modal from 'components/Modal'
|
||||
import Card, { OutlineCard } from 'components/Card'
|
||||
import { RowBetween, AutoRow } from 'components/Row'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { useActiveWeb3React } from 'hooks'
|
||||
import { getEtherscanLink } from 'utils'
|
||||
import { Currency, Token } from '@uniswap/sdk'
|
||||
import { wrappedCurrency } from 'utils/wrappedCurrency'
|
||||
import { useUnsupportedTokens } from '../../hooks/Tokens'
|
||||
|
||||
const DetailsFooter = styled.div<{ show: boolean }>`
|
||||
padding-top: calc(16px + 2rem);
|
||||
padding-bottom: 20px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
z-index: -1;
|
||||
|
||||
transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')};
|
||||
transition: transform 300ms ease-in-out;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const AddressText = styled(TYPE.blue)`
|
||||
font-size: 12px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
font-size: 10px;
|
||||
`}
|
||||
`
|
||||
|
||||
export default function UnsupportedCurrencyFooter({
|
||||
show,
|
||||
currencies
|
||||
}: {
|
||||
show: boolean
|
||||
currencies: (Currency | undefined)[]
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
const tokens =
|
||||
chainId && currencies
|
||||
? currencies.map(currency => {
|
||||
return wrappedCurrency(currency, chainId)
|
||||
})
|
||||
: []
|
||||
|
||||
const unsupportedTokens: { [address: string]: Token } = useUnsupportedTokens()
|
||||
|
||||
return (
|
||||
<DetailsFooter show={show}>
|
||||
<Modal isOpen={showDetails} onDismiss={() => setShowDetails(false)}>
|
||||
<Card padding="2rem">
|
||||
<AutoColumn gap="lg">
|
||||
<RowBetween>
|
||||
<TYPE.mediumHeader>Unsupported Assets</TYPE.mediumHeader>
|
||||
|
||||
<CloseIcon onClick={() => setShowDetails(false)} />
|
||||
</RowBetween>
|
||||
{tokens.map(token => {
|
||||
return (
|
||||
token &&
|
||||
unsupportedTokens &&
|
||||
Object.keys(unsupportedTokens).includes(token.address) && (
|
||||
<OutlineCard key={token.address?.concat('not-supported')}>
|
||||
<AutoColumn gap="10px">
|
||||
<AutoRow gap="5px" align="center">
|
||||
<CurrencyLogo currency={token} size={'24px'} />
|
||||
<TYPE.body fontWeight={500}>{token.symbol}</TYPE.body>
|
||||
</AutoRow>
|
||||
{chainId && (
|
||||
<ExternalLink href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
<AddressText>{token.address}</AddressText>
|
||||
</ExternalLink>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</OutlineCard>
|
||||
)
|
||||
)
|
||||
})}
|
||||
<AutoColumn gap="lg">
|
||||
<TYPE.body fontWeight={500}>
|
||||
Some assets are not available through this interface because they may not work well with our smart
|
||||
contract or we are unable to allow trading for legal reasons.
|
||||
</TYPE.body>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</Card>
|
||||
</Modal>
|
||||
<ButtonEmpty padding={'0'} onClick={() => setShowDetails(true)}>
|
||||
<TYPE.blue>Read more about unsupported assets</TYPE.blue>
|
||||
</ButtonEmpty>
|
||||
</DetailsFooter>
|
||||
)
|
||||
}
|
@ -7,6 +7,7 @@ import { AutoColumn } from '../Column'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
export const ArrowWrapper = styled.div<{ clickable: boolean }>`
|
||||
@ -145,3 +146,8 @@ export const SwapShowAcceptChanges = styled(AutoColumn)`
|
||||
border-radius: 12px;
|
||||
margin-top: 8px;
|
||||
`
|
||||
export const Separator = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
`
|
||||
|
@ -200,9 +200,11 @@ export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(
|
||||
|
||||
// used to ensure the user doesn't send so much ETH so they end up with <.01
|
||||
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
|
||||
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
export const BETTER_TRADE_LESS_HOPS_THRESHOLD = new Percent(JSBI.BigInt(50), JSBI.BigInt(10000))
|
||||
|
||||
export const ZERO_PERCENT = new Percent('0')
|
||||
export const ONE_HUNDRED_PERCENT = new Percent('1')
|
||||
|
||||
// SDN OFAC addresses
|
||||
export const BLOCKED_ADDRESSES: string[] = [
|
||||
'0x7F367cC41522cE07553e823bf3be79A889DEbe1B',
|
||||
|
@ -1,20 +1,41 @@
|
||||
// the Uniswap Default token list lives here
|
||||
export const DEFAULT_TOKEN_LIST_URL = 'tokens.uniswap.eth'
|
||||
|
||||
// used to mark unsupported tokens, these are hosted lists of unsupported tokens
|
||||
/**
|
||||
* @TODO add list from blockchain association
|
||||
*/
|
||||
export const UNSUPPORTED_LIST_URLS: string[] = []
|
||||
|
||||
const COMPOUND_LIST = 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json'
|
||||
const UMA_LIST = 'https://umaproject.org/uma.tokenlist.json'
|
||||
const AAVE_LIST = 'tokenlist.aave.eth'
|
||||
const SYNTHETIX_LIST = 'synths.snx.eth'
|
||||
const WRAPPED_LIST = 'wrapped.tokensoft.eth'
|
||||
const SET_LIST = 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json'
|
||||
const OPYN_LIST = 'https://raw.githubusercontent.com/opynfinance/opyn-tokenlist/master/opyn-v1.tokenlist.json'
|
||||
const ROLL_LIST = 'https://app.tryroll.com/tokens.json'
|
||||
const COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
|
||||
const CMC_ALL_LIST = 'defi.cmc.eth'
|
||||
const CMC_STABLECOIN = 'stablecoin.cmc.eth'
|
||||
const KLEROS_LIST = 't2crtokens.eth'
|
||||
|
||||
// lower index == higher priority for token import
|
||||
export const DEFAULT_LIST_OF_LISTS: string[] = [
|
||||
DEFAULT_TOKEN_LIST_URL,
|
||||
't2crtokens.eth', // kleros
|
||||
'tokens.1inch.eth', // 1inch
|
||||
'synths.snx.eth',
|
||||
'tokenlist.dharma.eth',
|
||||
'defi.cmc.eth',
|
||||
'erc20.cmc.eth',
|
||||
'stablecoin.cmc.eth',
|
||||
'tokenlist.zerion.eth',
|
||||
'tokenlist.aave.eth',
|
||||
'https://tokens.coingecko.com/uniswap/all.json',
|
||||
'https://app.tryroll.com/tokens.json',
|
||||
'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json',
|
||||
'https://defiprime.com/defiprime.tokenlist.json',
|
||||
'https://umaproject.org/uma.tokenlist.json'
|
||||
COMPOUND_LIST,
|
||||
AAVE_LIST,
|
||||
SYNTHETIX_LIST,
|
||||
UMA_LIST,
|
||||
WRAPPED_LIST,
|
||||
SET_LIST,
|
||||
OPYN_LIST,
|
||||
ROLL_LIST,
|
||||
COINGECKO_LIST,
|
||||
CMC_ALL_LIST,
|
||||
CMC_STABLECOIN,
|
||||
KLEROS_LIST,
|
||||
...UNSUPPORTED_LIST_URLS // need to load unsupported tokens as well
|
||||
]
|
||||
|
||||
// default lists to be 'active' aka searched across
|
||||
export const DEFAULT_ACTIVE_LIST_URLS: string[] = [DEFAULT_TOKEN_LIST_URL]
|
||||
|
406
src/constants/tokenLists/uniswap-default.tokenlist.json
Normal file
406
src/constants/tokenLists/uniswap-default.tokenlist.json
Normal file
@ -0,0 +1,406 @@
|
||||
{
|
||||
"name": "Uniswap Default List",
|
||||
"timestamp": "2021-01-11T23:59:53.688Z",
|
||||
"version": {
|
||||
"major": 1,
|
||||
"minor": 6,
|
||||
"patch": 0
|
||||
},
|
||||
"tags": {},
|
||||
"logoURI": "ipfs://QmNa8mQkrNKp1WEEeGjFezDmDeodkWRevGFN8JCV7b4Xir",
|
||||
"keywords": ["uniswap", "default"],
|
||||
"tokens": [
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9",
|
||||
"name": "Aave",
|
||||
"symbol": "AAVE",
|
||||
"decimals": 18,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110"
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0xfF20817765cB7f73d4bde2e66e067E58D11095C2",
|
||||
"name": "Amp",
|
||||
"symbol": "AMP",
|
||||
"decimals": 18,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/12409/thumb/amp-200x200.png?1599625397"
|
||||
},
|
||||
{
|
||||
"name": "Aragon Network Token",
|
||||
"address": "0x960b236A07cf122663c4303350609A66A7B288C0",
|
||||
"symbol": "ANT",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x960b236A07cf122663c4303350609A66A7B288C0/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Balancer",
|
||||
"address": "0xba100000625a3754423978a60c9317c58a424e3D",
|
||||
"symbol": "BAL",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xba100000625a3754423978a60c9317c58a424e3D/logo.png"
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55",
|
||||
"name": "Band Protocol",
|
||||
"symbol": "BAND",
|
||||
"decimals": 18,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/9545/thumb/band-protocol.png?1568730326"
|
||||
},
|
||||
{
|
||||
"name": "Bancor Network Token",
|
||||
"address": "0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C",
|
||||
"symbol": "BNT",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Compound",
|
||||
"address": "0xc00e94Cb662C3520282E6f5717214004A7f26888",
|
||||
"symbol": "COMP",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Curve DAO Token",
|
||||
"address": "0xD533a949740bb3306d119CC777fa900bA034cd52",
|
||||
"symbol": "CRV",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png"
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x41e5560054824eA6B0732E656E3Ad64E20e94E45",
|
||||
"name": "Civic",
|
||||
"symbol": "CVC",
|
||||
"decimals": 8,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/788/thumb/civic.png?1547034556"
|
||||
},
|
||||
{
|
||||
"name": "Dai Stablecoin",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"symbol": "DAI",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png"
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x0AbdAce70D3790235af448C88547603b945604ea",
|
||||
"name": "district0x",
|
||||
"symbol": "DNT",
|
||||
"decimals": 18,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/849/thumb/district0x.png?1547223762"
|
||||
},
|
||||
{
|
||||
"name": "Gnosis Token",
|
||||
"address": "0x6810e776880C02933D47DB1b9fc05908e5386b96",
|
||||
"symbol": "GNO",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6810e776880C02933D47DB1b9fc05908e5386b96/logo.png"
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0xc944E90C64B2c07662A292be6244BDf05Cda44a7",
|
||||
"name": "The Graph",
|
||||
"symbol": "GRT",
|
||||
"decimals": 18,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/13397/thumb/Graph_Token.png?1608145566"
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x85Eee30c52B0b379b046Fb0F85F4f3Dc3009aFEC",
|
||||
"name": "Keep Network",
|
||||
"symbol": "KEEP",
|
||||
"decimals": 18,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/3373/thumb/IuNzUb5b_400x400.jpg?1589526336"
|
||||
},
|
||||
{
|
||||
"name": "Kyber Network Crystal",
|
||||
"address": "0xdd974D5C2e2928deA5F71b9825b8b646686BD200",
|
||||
"symbol": "KNC",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdd974D5C2e2928deA5F71b9825b8b646686BD200/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "ChainLink Token",
|
||||
"address": "0x514910771AF9Ca656af840dff83E8264EcF986CA",
|
||||
"symbol": "LINK",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Loom Network",
|
||||
"address": "0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0",
|
||||
"symbol": "LOOM",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "LoopringCoin V2",
|
||||
"address": "0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD",
|
||||
"symbol": "LRC",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD/logo.png"
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942",
|
||||
"name": "Decentraland",
|
||||
"symbol": "MANA",
|
||||
"decimals": 18,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/878/thumb/decentraland-mana.png?1550108745"
|
||||
},
|
||||
{
|
||||
"name": "Maker",
|
||||
"address": "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2",
|
||||
"symbol": "MKR",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2/logo.png"
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0xec67005c4E498Ec7f55E092bd1d35cbC47C91892",
|
||||
"name": "Melon",
|
||||
"symbol": "MLN",
|
||||
"decimals": 18,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/605/thumb/melon.png?1547034295"
|
||||
},
|
||||
{
|
||||
"name": "Numeraire",
|
||||
"address": "0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671",
|
||||
"symbol": "NMR",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671/logo.png"
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x4fE83213D56308330EC302a8BD641f1d0113A4Cc",
|
||||
"name": "NuCypher",
|
||||
"symbol": "NU",
|
||||
"decimals": 18,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/3318/thumb/photo1198982838879365035.jpg?1547037916"
|
||||
},
|
||||
{
|
||||
"name": "Orchid",
|
||||
"address": "0x4575f41308EC1483f3d399aa9a2826d74Da13Deb",
|
||||
"symbol": "OXT",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4575f41308EC1483f3d399aa9a2826d74Da13Deb/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Republic Token",
|
||||
"address": "0x408e41876cCCDC0F92210600ef50372656052a38",
|
||||
"symbol": "REN",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x408e41876cCCDC0F92210600ef50372656052a38/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Reputation Augur v1",
|
||||
"address": "0x1985365e9f78359a9B6AD760e32412f4a445E862",
|
||||
"symbol": "REP",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1985365e9f78359a9B6AD760e32412f4a445E862/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Reputation Augur v2",
|
||||
"address": "0x221657776846890989a759BA2973e427DfF5C9bB",
|
||||
"symbol": "REPv2",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x221657776846890989a759BA2973e427DfF5C9bB/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Synthetix Network Token",
|
||||
"address": "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F",
|
||||
"symbol": "SNX",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Storj Token",
|
||||
"address": "0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC",
|
||||
"symbol": "STORJ",
|
||||
"decimals": 8,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC/logo.png"
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x8dAEBADE922dF735c38C80C7eBD708Af50815fAa",
|
||||
"name": "tBTC",
|
||||
"symbol": "TBTC",
|
||||
"decimals": 18,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/11224/thumb/tBTC.png?1589620754"
|
||||
},
|
||||
{
|
||||
"name": "UMA Voting Token v1",
|
||||
"address": "0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828",
|
||||
"symbol": "UMA",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Uniswap",
|
||||
"address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
|
||||
"symbol": "UNI",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg"
|
||||
},
|
||||
{
|
||||
"name": "Wrapped BTC",
|
||||
"address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
|
||||
"symbol": "WBTC",
|
||||
"decimals": 8,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Wrapped Ether",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"symbol": "WETH",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e",
|
||||
"name": "yearn finance",
|
||||
"symbol": "YFI",
|
||||
"decimals": 18,
|
||||
"logoURI": "https://assets.coingecko.com/coins/images/11849/thumb/yfi-192x192.png?1598325330"
|
||||
},
|
||||
{
|
||||
"name": "0x Protocol Token",
|
||||
"address": "0xE41d2489571d322189246DaFA5ebDe1F4699F498",
|
||||
"symbol": "ZRX",
|
||||
"decimals": 18,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xE41d2489571d322189246DaFA5ebDe1F4699F498/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Dai Stablecoin",
|
||||
"address": "0xaD6D458402F60fD3Bd25163575031ACDce07538D",
|
||||
"symbol": "DAI",
|
||||
"decimals": 18,
|
||||
"chainId": 3,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xaD6D458402F60fD3Bd25163575031ACDce07538D/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Uniswap",
|
||||
"address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
|
||||
"symbol": "UNI",
|
||||
"decimals": 18,
|
||||
"chainId": 3,
|
||||
"logoURI": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg"
|
||||
},
|
||||
{
|
||||
"name": "Wrapped Ether",
|
||||
"address": "0xc778417E063141139Fce010982780140Aa0cD5Ab",
|
||||
"symbol": "WETH",
|
||||
"decimals": 18,
|
||||
"chainId": 3,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc778417E063141139Fce010982780140Aa0cD5Ab/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Dai Stablecoin",
|
||||
"address": "0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735",
|
||||
"symbol": "DAI",
|
||||
"decimals": 18,
|
||||
"chainId": 4,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Maker",
|
||||
"address": "0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85",
|
||||
"symbol": "MKR",
|
||||
"decimals": 18,
|
||||
"chainId": 4,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Uniswap",
|
||||
"address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
|
||||
"symbol": "UNI",
|
||||
"decimals": 18,
|
||||
"chainId": 4,
|
||||
"logoURI": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg"
|
||||
},
|
||||
{
|
||||
"name": "Wrapped Ether",
|
||||
"address": "0xc778417E063141139Fce010982780140Aa0cD5Ab",
|
||||
"symbol": "WETH",
|
||||
"decimals": 18,
|
||||
"chainId": 4,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc778417E063141139Fce010982780140Aa0cD5Ab/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Uniswap",
|
||||
"address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
|
||||
"symbol": "UNI",
|
||||
"decimals": 18,
|
||||
"chainId": 5,
|
||||
"logoURI": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg"
|
||||
},
|
||||
{
|
||||
"name": "Wrapped Ether",
|
||||
"address": "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
|
||||
"symbol": "WETH",
|
||||
"decimals": 18,
|
||||
"chainId": 5,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Dai Stablecoin",
|
||||
"address": "0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa",
|
||||
"symbol": "DAI",
|
||||
"decimals": 18,
|
||||
"chainId": 42,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Maker",
|
||||
"address": "0xAaF64BFCC32d0F15873a02163e7E500671a4ffcD",
|
||||
"symbol": "MKR",
|
||||
"decimals": 18,
|
||||
"chainId": 42,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xAaF64BFCC32d0F15873a02163e7E500671a4ffcD/logo.png"
|
||||
},
|
||||
{
|
||||
"name": "Uniswap",
|
||||
"address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
|
||||
"symbol": "UNI",
|
||||
"decimals": 18,
|
||||
"chainId": 42,
|
||||
"logoURI": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg"
|
||||
},
|
||||
{
|
||||
"name": "Wrapped Ether",
|
||||
"address": "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
|
||||
"symbol": "WETH",
|
||||
"decimals": 18,
|
||||
"chainId": 42,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xd0A1E359811322d97991E03f863a0C30C2cF029C/logo.png"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Uniswap V2 Unsupported List",
|
||||
"timestamp": "2021-01-05T20:47:02.923Z",
|
||||
"version": {
|
||||
"major": 1,
|
||||
"minor": 0,
|
||||
"patch": 0
|
||||
},
|
||||
"tags": {},
|
||||
"logoURI": "ipfs://QmNa8mQkrNKp1WEEeGjFezDmDeodkWRevGFN8JCV7b4Xir",
|
||||
"keywords": ["uniswap", "unsupported"],
|
||||
"tokens": [
|
||||
{
|
||||
"name": "Gold Tether",
|
||||
"address": "0x4922a015c4407F87432B179bb209e125432E4a2A",
|
||||
"symbol": "XAUt",
|
||||
"decimals": 6,
|
||||
"chainId": 1,
|
||||
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4922a015c4407F87432B179bb209e125432E4a2A/logo.png"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,41 +1,118 @@
|
||||
import { TokenAddressMap, useDefaultTokenList, useUnsupportedTokenList } from './../state/lists/hooks'
|
||||
import { parseBytes32String } from '@ethersproject/strings'
|
||||
import { Currency, ETHER, Token, currencyEquals } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useSelectedTokenList } from '../state/lists/hooks'
|
||||
import { useCombinedActiveList, useCombinedInactiveList } from '../state/lists/hooks'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useUserAddedTokens } from '../state/user/hooks'
|
||||
import { isAddress } from '../utils'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { useBytes32TokenContract, useTokenContract } from './useContract'
|
||||
import { filterTokens } from '../components/SearchModal/filtering'
|
||||
import { arrayify } from 'ethers/lib/utils'
|
||||
|
||||
export function useAllTokens(): { [address: string]: Token } {
|
||||
// reduce token map into standard address <-> Token mapping, optionally include user added tokens
|
||||
function useTokensFromMap(tokenMap: TokenAddressMap, includeUserAdded: boolean): { [address: string]: Token } {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
const allTokens = useSelectedTokenList()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!chainId) return {}
|
||||
return (
|
||||
userAddedTokens
|
||||
// reduce into all ALL_TOKENS filtered by the current chain
|
||||
.reduce<{ [address: string]: Token }>(
|
||||
(tokenMap, token) => {
|
||||
tokenMap[token.address] = token
|
||||
return tokenMap
|
||||
},
|
||||
// must make a copy because reduce modifies the map, and we do not
|
||||
// want to make a copy in every iteration
|
||||
{ ...allTokens[chainId] }
|
||||
)
|
||||
)
|
||||
}, [chainId, userAddedTokens, allTokens])
|
||||
|
||||
// reduce to just tokens
|
||||
const mapWithoutUrls = Object.keys(tokenMap[chainId]).reduce<{ [address: string]: Token }>((newMap, address) => {
|
||||
newMap[address] = tokenMap[chainId][address].token
|
||||
return newMap
|
||||
}, {})
|
||||
|
||||
if (includeUserAdded) {
|
||||
return (
|
||||
userAddedTokens
|
||||
// reduce into all ALL_TOKENS filtered by the current chain
|
||||
.reduce<{ [address: string]: Token }>(
|
||||
(tokenMap, token) => {
|
||||
tokenMap[token.address] = token
|
||||
return tokenMap
|
||||
},
|
||||
// must make a copy because reduce modifies the map, and we do not
|
||||
// want to make a copy in every iteration
|
||||
{ ...mapWithoutUrls }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return mapWithoutUrls
|
||||
}, [chainId, userAddedTokens, tokenMap, includeUserAdded])
|
||||
}
|
||||
|
||||
export function useDefaultTokens(): { [address: string]: Token } {
|
||||
const defaultList = useDefaultTokenList()
|
||||
return useTokensFromMap(defaultList, false)
|
||||
}
|
||||
|
||||
export function useAllTokens(): { [address: string]: Token } {
|
||||
const allTokens = useCombinedActiveList()
|
||||
return useTokensFromMap(allTokens, true)
|
||||
}
|
||||
|
||||
export function useAllInactiveTokens(): { [address: string]: Token } {
|
||||
// get inactive tokens
|
||||
const inactiveTokensMap = useCombinedInactiveList()
|
||||
const inactiveTokens = useTokensFromMap(inactiveTokensMap, false)
|
||||
|
||||
// filter out any token that are on active list
|
||||
const activeTokensAddresses = Object.keys(useAllTokens())
|
||||
const filteredInactive = activeTokensAddresses
|
||||
? Object.keys(inactiveTokens).reduce<{ [address: string]: Token }>((newMap, address) => {
|
||||
if (!activeTokensAddresses.includes(address)) {
|
||||
newMap[address] = inactiveTokens[address]
|
||||
}
|
||||
return newMap
|
||||
}, {})
|
||||
: inactiveTokens
|
||||
|
||||
return filteredInactive
|
||||
}
|
||||
|
||||
export function useUnsupportedTokens(): { [address: string]: Token } {
|
||||
const unsupportedTokensMap = useUnsupportedTokenList()
|
||||
return useTokensFromMap(unsupportedTokensMap, false)
|
||||
}
|
||||
|
||||
export function useIsTokenActive(token: Token | undefined | null): boolean {
|
||||
const activeTokens = useAllTokens()
|
||||
|
||||
if (!activeTokens || !token) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !!activeTokens[token.address]
|
||||
}
|
||||
|
||||
// used to detect extra search results
|
||||
export function useFoundOnInactiveList(searchQuery: string): Token[] | undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const inactiveTokens = useAllInactiveTokens()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!chainId || searchQuery === '') {
|
||||
return undefined
|
||||
} else {
|
||||
const tokens = filterTokens(Object.values(inactiveTokens), searchQuery)
|
||||
return tokens
|
||||
}
|
||||
}, [chainId, inactiveTokens, searchQuery])
|
||||
}
|
||||
|
||||
// Check if currency is included in custom list from user storage
|
||||
export function useIsUserAddedToken(currency: Currency): boolean {
|
||||
export function useIsUserAddedToken(currency: Currency | undefined | null): boolean {
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
|
||||
if (!currency) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !!userAddedTokens.find(token => currencyEquals(currency, token))
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { PairState, usePairs } from '../data/Reserves'
|
||||
import { wrappedCurrency } from '../utils/wrappedCurrency'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { useUnsupportedTokens } from './Tokens'
|
||||
import { useUserSingleHopOnly } from 'state/user/hooks'
|
||||
|
||||
function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
|
||||
@ -147,3 +148,23 @@ export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: Curr
|
||||
return null
|
||||
}, [currencyIn, currencyAmountOut, allowedPairs, singleHopOnly])
|
||||
}
|
||||
|
||||
export function useIsTransactionUnsupported(currencyIn?: Currency, currencyOut?: Currency): boolean {
|
||||
const unsupportedToken: { [address: string]: Token } = useUnsupportedTokens()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const tokenIn = wrappedCurrency(currencyIn, chainId)
|
||||
const tokenOut = wrappedCurrency(currencyOut, chainId)
|
||||
|
||||
// if unsupported list loaded & either token on list, mark as unsupported
|
||||
if (unsupportedToken) {
|
||||
if (tokenIn && Object.keys(unsupportedToken).includes(tokenIn.address)) {
|
||||
return true
|
||||
}
|
||||
if (tokenOut && Object.keys(unsupportedToken).includes(tokenOut.address)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { shade } from 'polished'
|
||||
import Vibrant from 'node-vibrant'
|
||||
import { hex } from 'wcag-contrast'
|
||||
import { Token, ChainId } from '@uniswap/sdk'
|
||||
import uriToHttp from 'utils/uriToHttp'
|
||||
|
||||
async function getColorFromToken(token: Token): Promise<string | null> {
|
||||
if (token.chainId === ChainId.RINKEBY && token.address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
|
||||
@ -28,6 +29,20 @@ async function getColorFromToken(token: Token): Promise<string | null> {
|
||||
.catch(() => null)
|
||||
}
|
||||
|
||||
async function getColorFromUriPath(uri: string): Promise<string | null> {
|
||||
const formattedPath = uriToHttp(uri)[0]
|
||||
|
||||
return Vibrant.from(formattedPath)
|
||||
.getPalette()
|
||||
.then(palette => {
|
||||
if (palette?.Vibrant) {
|
||||
return palette.Vibrant.hex
|
||||
}
|
||||
return null
|
||||
})
|
||||
.catch(() => null)
|
||||
}
|
||||
|
||||
export function useColor(token?: Token) {
|
||||
const [color, setColor] = useState('#2172E5')
|
||||
|
||||
@ -50,3 +65,26 @@ export function useColor(token?: Token) {
|
||||
|
||||
return color
|
||||
}
|
||||
|
||||
export function useListColor(listImageUri?: string) {
|
||||
const [color, setColor] = useState('#2172E5')
|
||||
|
||||
useLayoutEffect(() => {
|
||||
let stale = false
|
||||
|
||||
if (listImageUri) {
|
||||
getColorFromUriPath(listImageUri).then(color => {
|
||||
if (!stale && color !== null) {
|
||||
setColor(color)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
setColor('#2172E5')
|
||||
}
|
||||
}, [listImageUri])
|
||||
|
||||
return color
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import getTokenList from '../utils/getTokenList'
|
||||
import resolveENSContentHash from '../utils/resolveENSContentHash'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
export function useFetchListCallback(): (listUrl: string) => Promise<TokenList> {
|
||||
export function useFetchListCallback(): (listUrl: string, sendDispatch?: boolean) => Promise<TokenList> {
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
@ -30,18 +30,19 @@ export function useFetchListCallback(): (listUrl: string) => Promise<TokenList>
|
||||
[chainId, library]
|
||||
)
|
||||
|
||||
// note: prevent dispatch if using for list search or unsupported list
|
||||
return useCallback(
|
||||
async (listUrl: string) => {
|
||||
async (listUrl: string, sendDispatch = true) => {
|
||||
const requestId = nanoid()
|
||||
dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
|
||||
sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
|
||||
return getTokenList(listUrl, ensResolver)
|
||||
.then(tokenList => {
|
||||
dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
|
||||
sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
|
||||
return tokenList
|
||||
})
|
||||
.catch(error => {
|
||||
console.debug(`Failed to get list at url ${listUrl}`, error)
|
||||
dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
|
||||
sendDispatch && dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
|
||||
throw error
|
||||
})
|
||||
},
|
||||
|
6
src/hooks/useTheme.ts
Normal file
6
src/hooks/useTheme.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { useContext } from 'react'
|
||||
|
||||
export default function useTheme() {
|
||||
return useContext(ThemeContext)
|
||||
}
|
@ -38,6 +38,8 @@ import { Dots, Wrapper } from '../Pool/styleds'
|
||||
import { ConfirmAddModalBottom } from './ConfirmAddModalBottom'
|
||||
import { currencyId } from '../../utils/currencyId'
|
||||
import { PoolPriceBar } from './PoolPriceBar'
|
||||
import { useIsTransactionUnsupported } from 'hooks/Trades'
|
||||
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
|
||||
|
||||
export default function AddLiquidity({
|
||||
match: {
|
||||
@ -76,6 +78,7 @@ export default function AddLiquidity({
|
||||
poolTokenPercentage,
|
||||
error
|
||||
} = useDerivedMintInfo(currencyA ?? undefined, currencyB ?? undefined)
|
||||
|
||||
const { onFieldAInput, onFieldBInput } = useMintActionHandlers(noLiquidity)
|
||||
|
||||
const isValid = !error
|
||||
@ -304,6 +307,8 @@ export default function AddLiquidity({
|
||||
|
||||
const isCreate = history.location.pathname.includes('/create')
|
||||
|
||||
const addIsUnsupported = useIsTransactionUnsupported(currencies?.CURRENCY_A, currencies?.CURRENCY_B)
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBody>
|
||||
@ -326,7 +331,7 @@ export default function AddLiquidity({
|
||||
/>
|
||||
<AutoColumn gap="20px">
|
||||
{noLiquidity ||
|
||||
(isCreate && (
|
||||
(isCreate ? (
|
||||
<ColumnCenter>
|
||||
<BlueCard>
|
||||
<AutoColumn gap="10px">
|
||||
@ -342,6 +347,18 @@ export default function AddLiquidity({
|
||||
</AutoColumn>
|
||||
</BlueCard>
|
||||
</ColumnCenter>
|
||||
) : (
|
||||
<ColumnCenter>
|
||||
<BlueCard>
|
||||
<AutoColumn gap="10px">
|
||||
<TYPE.link fontWeight={400} color={'primaryText1'}>
|
||||
<b>Tip:</b> When you add liquidity, you will receive pool tokens representing your position.
|
||||
These tokens automatically earn fees proportional to your share of the pool, and can be redeemed
|
||||
at any time.
|
||||
</TYPE.link>
|
||||
</AutoColumn>
|
||||
</BlueCard>
|
||||
</ColumnCenter>
|
||||
))}
|
||||
<CurrencyInputPanel
|
||||
value={formattedAmounts[Field.CURRENCY_A]}
|
||||
@ -390,7 +407,11 @@ export default function AddLiquidity({
|
||||
</>
|
||||
)}
|
||||
|
||||
{!account ? (
|
||||
{addIsUnsupported ? (
|
||||
<ButtonPrimary disabled={true}>
|
||||
<TYPE.main mb="4px">Unsupported Asset</TYPE.main>
|
||||
</ButtonPrimary>
|
||||
) : !account ? (
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : (
|
||||
<AutoColumn gap={'md'}>
|
||||
@ -444,12 +465,18 @@ export default function AddLiquidity({
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{pair && !noLiquidity && pairState !== PairState.INVALID ? (
|
||||
<AutoColumn style={{ minWidth: '20rem', width: '100%', maxWidth: '400px', marginTop: '1rem' }}>
|
||||
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
|
||||
</AutoColumn>
|
||||
) : null}
|
||||
{!addIsUnsupported ? (
|
||||
pair && !noLiquidity && pairState !== PairState.INVALID ? (
|
||||
<AutoColumn style={{ minWidth: '20rem', width: '100%', maxWidth: '400px', marginTop: '1rem' }}>
|
||||
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
|
||||
</AutoColumn>
|
||||
) : null
|
||||
) : (
|
||||
<UnsupportedCurrencyFooter
|
||||
show={addIsUnsupported}
|
||||
currencies={[currencies.CURRENCY_A, currencies.CURRENCY_B]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ import RemoveLiquidity from './RemoveLiquidity'
|
||||
import { RedirectOldRemoveLiquidityPathStructure } from './RemoveLiquidity/redirects'
|
||||
import Swap from './Swap'
|
||||
import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
|
||||
|
||||
import Vote from './Vote'
|
||||
import VotePage from './Vote/VotePage'
|
||||
|
||||
|
@ -9,7 +9,7 @@ export const BodyWrapper = styled.div`
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
border-radius: 30px;
|
||||
padding: 1rem;
|
||||
/* padding: 1rem; */
|
||||
`
|
||||
|
||||
/**
|
||||
|
@ -7,7 +7,6 @@ import { SearchInput } from '../../components/SearchModal/styleds'
|
||||
import { useAllTokenV1Exchanges } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import { useSelectedTokenList } from '../../state/lists/hooks'
|
||||
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
|
||||
import { BackArrow, TYPE } from '../../theme'
|
||||
import { LightCard } from '../../components/Card'
|
||||
@ -18,6 +17,7 @@ import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { useAddUserToken } from '../../state/user/hooks'
|
||||
import { isTokenOnList } from '../../utils'
|
||||
import { useCombinedActiveList } from '../../state/lists/hooks'
|
||||
|
||||
export default function MigrateV1() {
|
||||
const theme = useContext(ThemeContext)
|
||||
@ -28,7 +28,7 @@ export default function MigrateV1() {
|
||||
|
||||
// automatically add the search token
|
||||
const token = useToken(tokenSearch)
|
||||
const selectedTokenListTokens = useSelectedTokenList()
|
||||
const selectedTokenListTokens = useCombinedActiveList()
|
||||
const isOnSelectedList = isTokenOnList(selectedTokenListTokens, token ?? undefined)
|
||||
const allTokens = useAllTokens()
|
||||
const addToken = useAddUserToken()
|
||||
|
@ -165,7 +165,13 @@ export default function Pool() {
|
||||
<ResponsiveButtonSecondary as={Link} padding="6px 8px" to="/create/ETH">
|
||||
Create a pair
|
||||
</ResponsiveButtonSecondary>
|
||||
<ResponsiveButtonPrimary id="join-pool-button" as={Link} padding="6px 8px" to="/add/ETH">
|
||||
<ResponsiveButtonPrimary
|
||||
id="join-pool-button"
|
||||
as={Link}
|
||||
padding="6px 8px"
|
||||
borderRadius="12px"
|
||||
to="/add/ETH"
|
||||
>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Add Liquidity
|
||||
</Text>
|
||||
|
@ -3,6 +3,7 @@ import styled from 'styled-components'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
export const ClickableText = styled(Text)`
|
||||
|
@ -18,6 +18,8 @@ import { StyledInternalLink } from '../../theme'
|
||||
import { currencyId } from '../../utils/currencyId'
|
||||
import AppBody from '../AppBody'
|
||||
import { Dots } from '../Pool/styleds'
|
||||
import { BlueCard } from '../../components/Card'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
enum Fields {
|
||||
TOKEN0 = 0,
|
||||
@ -79,7 +81,14 @@ export default function PoolFinder() {
|
||||
return (
|
||||
<AppBody>
|
||||
<FindPoolTabs />
|
||||
<AutoColumn gap="md">
|
||||
<AutoColumn style={{ padding: '1rem' }} gap="md">
|
||||
<BlueCard>
|
||||
<AutoColumn gap="10px">
|
||||
<TYPE.link fontWeight={400} color={'primaryText1'}>
|
||||
<b>Tip:</b> Use this tool to find pairs that don't automatically appear in the interface.
|
||||
</TYPE.link>
|
||||
</AutoColumn>
|
||||
</BlueCard>
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
|
@ -9,7 +9,7 @@ import { RouteComponentProps } from 'react-router'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ButtonPrimary, ButtonLight, ButtonError, ButtonConfirmed } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { BlueCard, LightCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
@ -492,6 +492,14 @@ export default function RemoveLiquidity({
|
||||
pendingText={pendingText}
|
||||
/>
|
||||
<AutoColumn gap="md">
|
||||
<BlueCard>
|
||||
<AutoColumn gap="10px">
|
||||
<TYPE.link fontWeight={400} color={'primaryText1'}>
|
||||
<b>Tip:</b> Removing pool tokens converts your position back into underlying tokens at the current
|
||||
rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.
|
||||
</TYPE.link>
|
||||
</AutoColumn>
|
||||
</BlueCard>
|
||||
<LightCard>
|
||||
<AutoColumn gap="20px">
|
||||
<RowBetween>
|
||||
|
@ -19,11 +19,12 @@ import { ArrowWrapper, BottomGrouping, SwapCallbackError, Wrapper } from '../../
|
||||
import TradePrice from '../../components/swap/TradePrice'
|
||||
import TokenWarningModal from '../../components/TokenWarningModal'
|
||||
import ProgressSteps from '../../components/ProgressSteps'
|
||||
import SwapHeader from '../../components/swap/SwapHeader'
|
||||
|
||||
import { BETTER_TRADE_LINK_THRESHOLD, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { getTradeVersion } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useCurrency } from '../../hooks/Tokens'
|
||||
import { useCurrency, useDefaultTokens } from '../../hooks/Tokens'
|
||||
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
|
||||
import useENSAddress from '../../hooks/useENSAddress'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
@ -44,6 +45,8 @@ import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { ClickableText } from '../Pool/styleds'
|
||||
import Loader from '../../components/Loader'
|
||||
import { useIsTransactionUnsupported } from 'hooks/Trades'
|
||||
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
|
||||
import { isTradeBetter } from 'utils/trades'
|
||||
|
||||
export default function Swap() {
|
||||
@ -63,6 +66,14 @@ export default function Swap() {
|
||||
setDismissTokenWarning(true)
|
||||
}, [])
|
||||
|
||||
// dismiss warning if all imported tokens are in default list
|
||||
const defaultTokens = useDefaultTokens()
|
||||
const importTokensNotInDefault =
|
||||
urlLoadedTokens &&
|
||||
urlLoadedTokens.filter((token: Token) => {
|
||||
return !Boolean(token.address in defaultTokens)
|
||||
}).length > 0
|
||||
|
||||
const { account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
@ -101,12 +112,8 @@ export default function Swap() {
|
||||
const trade = showWrap ? undefined : tradesByVersion[toggledVersion]
|
||||
const defaultTrade = showWrap ? undefined : tradesByVersion[DEFAULT_VERSION]
|
||||
|
||||
const betterTradeLinkVersion: Version | undefined =
|
||||
toggledVersion === Version.v2 && isTradeBetter(v2Trade, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
|
||||
? Version.v1
|
||||
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, v2Trade)
|
||||
? Version.v2
|
||||
: undefined
|
||||
const betterTradeLinkV2: Version | undefined =
|
||||
toggledVersion === Version.v1 && isTradeBetter(v1Trade, v2Trade) ? Version.v2 : undefined
|
||||
|
||||
const parsedAmounts = showWrap
|
||||
? {
|
||||
@ -282,15 +289,19 @@ export default function Swap() {
|
||||
onCurrencySelection
|
||||
])
|
||||
|
||||
const swapIsUnsupported = useIsTransactionUnsupported(currencies?.INPUT, currencies?.OUTPUT)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TokenWarningModal
|
||||
isOpen={urlLoadedTokens.length > 0 && !dismissTokenWarning}
|
||||
isOpen={urlLoadedTokens.length > 0 && !dismissTokenWarning && importTokensNotInDefault}
|
||||
tokens={urlLoadedTokens}
|
||||
onConfirm={handleConfirmTokenWarning}
|
||||
/>
|
||||
<SwapPoolTabs active={'swap'} />
|
||||
<AppBody>
|
||||
<SwapPoolTabs active={'swap'} />
|
||||
<SwapHeader />
|
||||
{/* <Separator /> */}
|
||||
<Wrapper id="swap-page">
|
||||
<ConfirmSwapModal
|
||||
isOpen={showConfirm}
|
||||
@ -363,8 +374,8 @@ export default function Swap() {
|
||||
) : null}
|
||||
|
||||
{showWrap ? null : (
|
||||
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
|
||||
<AutoColumn gap="4px">
|
||||
<Card padding={showWrap ? '.25rem 1rem 0 1rem' : '0px'} borderRadius={'20px'}>
|
||||
<AutoColumn gap="8px" style={{ padding: '0 16px' }}>
|
||||
{Boolean(trade) && (
|
||||
<RowBetween align="center">
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2}>
|
||||
@ -392,7 +403,11 @@ export default function Swap() {
|
||||
)}
|
||||
</AutoColumn>
|
||||
<BottomGrouping>
|
||||
{!account ? (
|
||||
{swapIsUnsupported ? (
|
||||
<ButtonPrimary disabled={true}>
|
||||
<TYPE.main mb="4px">Unsupported Asset</TYPE.main>
|
||||
</ButtonPrimary>
|
||||
) : !account ? (
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : showWrap ? (
|
||||
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap}>
|
||||
@ -485,15 +500,19 @@ export default function Swap() {
|
||||
</Column>
|
||||
)}
|
||||
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
|
||||
{betterTradeLinkVersion ? (
|
||||
<BetterTradeLink version={betterTradeLinkVersion} />
|
||||
{betterTradeLinkV2 && !swapIsUnsupported && toggledVersion === Version.v1 ? (
|
||||
<BetterTradeLink version={betterTradeLinkV2} />
|
||||
) : toggledVersion !== DEFAULT_VERSION && defaultTrade ? (
|
||||
<DefaultVersionLink />
|
||||
) : null}
|
||||
</BottomGrouping>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
<AdvancedSwapDetailsDropdown trade={trade} />
|
||||
{!swapIsUnsupported ? (
|
||||
<AdvancedSwapDetailsDropdown trade={trade} />
|
||||
) : (
|
||||
<UnsupportedCurrencyFooter show={swapIsUnsupported} currencies={[currencies.INPUT, currencies.OUTPUT]} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -10,9 +10,14 @@ export const fetchTokenList: Readonly<{
|
||||
fulfilled: createAction('lists/fetchTokenList/fulfilled'),
|
||||
rejected: createAction('lists/fetchTokenList/rejected')
|
||||
}
|
||||
|
||||
export const acceptListUpdate = createAction<string>('lists/acceptListUpdate')
|
||||
// add and remove from list options
|
||||
export const addList = createAction<string>('lists/addList')
|
||||
export const removeList = createAction<string>('lists/removeList')
|
||||
export const selectList = createAction<string>('lists/selectList')
|
||||
|
||||
// select which lists to search across from loaded lists
|
||||
export const enableList = createAction<string>('lists/enableList')
|
||||
export const disableList = createAction<string>('lists/disableList')
|
||||
|
||||
// versioning
|
||||
export const acceptListUpdate = createAction<string>('lists/acceptListUpdate')
|
||||
export const rejectVersionUpdate = createAction<Version>('lists/rejectVersionUpdate')
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { UNSUPPORTED_LIST_URLS } from './../../constants/lists'
|
||||
import DEFAULT_TOKEN_LIST from 'constants/tokenLists/uniswap-default.tokenlist.json'
|
||||
import { ChainId, Token } from '@uniswap/sdk'
|
||||
import { Tags, TokenInfo, TokenList } from '@uniswap/token-lists'
|
||||
import { useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { AppState } from '../index'
|
||||
import sortByListPriority from 'utils/listSort'
|
||||
import UNSUPPORTED_TOKEN_LIST from '../../constants/tokenLists/uniswap-v2-unsupported.tokenlist.json'
|
||||
|
||||
type TagDetails = Tags[keyof Tags]
|
||||
export interface TagInfo extends TagDetails {
|
||||
@ -25,7 +29,9 @@ export class WrappedTokenInfo extends Token {
|
||||
}
|
||||
}
|
||||
|
||||
export type TokenAddressMap = Readonly<{ [chainId in ChainId]: Readonly<{ [tokenAddress: string]: WrappedTokenInfo }> }>
|
||||
export type TokenAddressMap = Readonly<
|
||||
{ [chainId in ChainId]: Readonly<{ [tokenAddress: string]: { token: WrappedTokenInfo; list: TokenList } }> }
|
||||
>
|
||||
|
||||
/**
|
||||
* An empty result, useful as a default.
|
||||
@ -60,7 +66,10 @@ export function listToTokenMap(list: TokenList): TokenAddressMap {
|
||||
...tokenMap,
|
||||
[token.chainId]: {
|
||||
...tokenMap[token.chainId],
|
||||
[token.address]: token
|
||||
[token.address]: {
|
||||
token,
|
||||
list: list
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -70,49 +79,99 @@ export function listToTokenMap(list: TokenList): TokenAddressMap {
|
||||
return map
|
||||
}
|
||||
|
||||
export function useTokenList(url: string | undefined): TokenAddressMap {
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
return useMemo(() => {
|
||||
if (!url) return EMPTY_LIST
|
||||
const current = lists[url]?.current
|
||||
if (!current) return EMPTY_LIST
|
||||
try {
|
||||
return listToTokenMap(current)
|
||||
} catch (error) {
|
||||
console.error('Could not show token list due to error', error)
|
||||
return EMPTY_LIST
|
||||
}
|
||||
}, [lists, url])
|
||||
export function useAllLists(): {
|
||||
readonly [url: string]: {
|
||||
readonly current: TokenList | null
|
||||
readonly pendingUpdate: TokenList | null
|
||||
readonly loadingRequestId: string | null
|
||||
readonly error: string | null
|
||||
}
|
||||
} {
|
||||
return useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
}
|
||||
|
||||
export function useSelectedListUrl(): string | undefined {
|
||||
return useSelector<AppState, AppState['lists']['selectedListUrl']>(state => state.lists.selectedListUrl)
|
||||
}
|
||||
|
||||
export function useSelectedTokenList(): TokenAddressMap {
|
||||
return useTokenList(useSelectedListUrl())
|
||||
}
|
||||
|
||||
export function useSelectedListInfo(): { current: TokenList | null; pending: TokenList | null; loading: boolean } {
|
||||
const selectedUrl = useSelectedListUrl()
|
||||
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const list = selectedUrl ? listsByUrl[selectedUrl] : undefined
|
||||
function combineMaps(map1: TokenAddressMap, map2: TokenAddressMap): TokenAddressMap {
|
||||
return {
|
||||
current: list?.current ?? null,
|
||||
pending: list?.pendingUpdate ?? null,
|
||||
loading: list?.loadingRequestId !== null
|
||||
1: { ...map1[1], ...map2[1] },
|
||||
3: { ...map1[3], ...map2[3] },
|
||||
4: { ...map1[4], ...map2[4] },
|
||||
5: { ...map1[5], ...map2[5] },
|
||||
42: { ...map1[42], ...map2[42] }
|
||||
}
|
||||
}
|
||||
|
||||
// returns all downloaded current lists
|
||||
export function useAllLists(): TokenList[] {
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
// merge tokens contained within lists from urls
|
||||
function useCombinedTokenMapFromUrls(urls: string[] | undefined): TokenAddressMap {
|
||||
const lists = useAllLists()
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.keys(lists)
|
||||
.map(url => lists[url].current)
|
||||
.filter((l): l is TokenList => Boolean(l)),
|
||||
[lists]
|
||||
return useMemo(() => {
|
||||
if (!urls) return EMPTY_LIST
|
||||
|
||||
return (
|
||||
urls
|
||||
.slice()
|
||||
// sort by priority so top priority goes last
|
||||
.sort(sortByListPriority)
|
||||
.reduce((allTokens, currentUrl) => {
|
||||
const current = lists[currentUrl]?.current
|
||||
if (!current) return allTokens
|
||||
try {
|
||||
const newTokens = Object.assign(listToTokenMap(current))
|
||||
return combineMaps(allTokens, newTokens)
|
||||
} catch (error) {
|
||||
console.error('Could not show token list due to error', error)
|
||||
return allTokens
|
||||
}
|
||||
}, EMPTY_LIST)
|
||||
)
|
||||
}, [lists, urls])
|
||||
}
|
||||
|
||||
// filter out unsupported lists
|
||||
export function useActiveListUrls(): string[] | undefined {
|
||||
return useSelector<AppState, AppState['lists']['activeListUrls']>(state => state.lists.activeListUrls)?.filter(
|
||||
url => !UNSUPPORTED_LIST_URLS.includes(url)
|
||||
)
|
||||
}
|
||||
|
||||
export function useInactiveListUrls(): string[] {
|
||||
const lists = useAllLists()
|
||||
const allActiveListUrls = useActiveListUrls()
|
||||
return Object.keys(lists).filter(url => !allActiveListUrls?.includes(url) && !UNSUPPORTED_LIST_URLS.includes(url))
|
||||
}
|
||||
|
||||
// get all the tokens from active lists, combine with local default tokens
|
||||
export function useCombinedActiveList(): TokenAddressMap {
|
||||
const activeListUrls = useActiveListUrls()
|
||||
const activeTokens = useCombinedTokenMapFromUrls(activeListUrls)
|
||||
const defaultTokenMap = listToTokenMap(DEFAULT_TOKEN_LIST)
|
||||
return combineMaps(activeTokens, defaultTokenMap)
|
||||
}
|
||||
|
||||
// all tokens from inactive lists
|
||||
export function useCombinedInactiveList(): TokenAddressMap {
|
||||
const allInactiveListUrls: string[] = useInactiveListUrls()
|
||||
return useCombinedTokenMapFromUrls(allInactiveListUrls)
|
||||
}
|
||||
|
||||
// used to hide warnings on import for default tokens
|
||||
export function useDefaultTokenList(): TokenAddressMap {
|
||||
return listToTokenMap(DEFAULT_TOKEN_LIST)
|
||||
}
|
||||
|
||||
// list of tokens not supported on interface, used to show warnings and prevent swaps and adds
|
||||
export function useUnsupportedTokenList(): TokenAddressMap {
|
||||
// get hard coded unsupported tokens
|
||||
const localUnsupportedListMap = listToTokenMap(UNSUPPORTED_TOKEN_LIST)
|
||||
|
||||
// get any loaded unsupported tokens
|
||||
const loadedUnsupportedListMap = useCombinedTokenMapFromUrls(UNSUPPORTED_LIST_URLS)
|
||||
|
||||
// format into one token address map
|
||||
return combineMaps(localUnsupportedListMap, loadedUnsupportedListMap)
|
||||
}
|
||||
|
||||
export function useIsListActive(url: string): boolean {
|
||||
const activeListUrls = useActiveListUrls()
|
||||
return Boolean(activeListUrls?.includes(url))
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { DEFAULT_ACTIVE_LIST_URLS } from './../../constants/lists'
|
||||
import { createStore, Store } from 'redux'
|
||||
import { DEFAULT_LIST_OF_LISTS, DEFAULT_TOKEN_LIST_URL } from '../../constants/lists'
|
||||
import { updateVersion } from '../global/actions'
|
||||
import { fetchTokenList, acceptListUpdate, addList, removeList, selectList } from './actions'
|
||||
import { fetchTokenList, acceptListUpdate, addList, removeList, enableList } from './actions'
|
||||
import reducer, { ListsState } from './reducer'
|
||||
|
||||
const STUB_TOKEN_LIST = {
|
||||
@ -30,7 +31,7 @@ describe('list reducer', () => {
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
|
||||
@ -61,7 +62,7 @@ describe('list reducer', () => {
|
||||
loadingRequestId: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
|
||||
store.dispatch(fetchTokenList.pending({ requestId: 'request-id', url: 'fake-url' }))
|
||||
@ -74,7 +75,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -93,7 +94,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
|
||||
@ -113,7 +114,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
|
||||
@ -134,7 +135,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
it('does not save to current if list is newer minor version', () => {
|
||||
@ -154,7 +155,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: MINOR_UPDATED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
it('does not save to pending if list is newer major version', () => {
|
||||
@ -174,7 +175,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: MAJOR_UPDATED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -184,7 +185,7 @@ describe('list reducer', () => {
|
||||
store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' }))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
|
||||
@ -198,7 +199,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' }))
|
||||
expect(store.getState()).toEqual({
|
||||
@ -210,7 +211,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -228,7 +229,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
it('no op for existing list', () => {
|
||||
@ -241,7 +242,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(addList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
@ -253,7 +254,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -269,7 +270,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(acceptListUpdate('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
@ -281,7 +282,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -297,15 +298,15 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(removeList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
})
|
||||
it('selects the default list if removed list was selected', () => {
|
||||
it('Removes from active lists if active list is removed', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -315,18 +316,18 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url'
|
||||
activeListUrls: ['fake-url']
|
||||
})
|
||||
store.dispatch(removeList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {},
|
||||
selectedListUrl: 'tokens.uniswap.eth'
|
||||
activeListUrls: []
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectList', () => {
|
||||
it('sets the selected list url', () => {
|
||||
describe('enableList', () => {
|
||||
it('enables a list url', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -336,9 +337,9 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(selectList('fake-url'))
|
||||
store.dispatch(enableList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -348,10 +349,10 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url'
|
||||
activeListUrls: ['fake-url']
|
||||
})
|
||||
})
|
||||
it('selects if not present already', () => {
|
||||
it('adds to url keys if not present already on enable', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -361,9 +362,9 @@ describe('list reducer', () => {
|
||||
pendingUpdate: PATCHED_STUB_LIST
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(selectList('fake-url-invalid'))
|
||||
store.dispatch(enableList('fake-url-invalid'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -379,10 +380,10 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url-invalid'
|
||||
activeListUrls: ['fake-url-invalid']
|
||||
})
|
||||
})
|
||||
it('works if list already added', () => {
|
||||
it('enable works if list already added', () => {
|
||||
store = createStore(reducer, {
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -392,9 +393,9 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(selectList('fake-url'))
|
||||
store.dispatch(enableList('fake-url'))
|
||||
expect(store.getState()).toEqual({
|
||||
byUrl: {
|
||||
'fake-url': {
|
||||
@ -404,7 +405,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: 'fake-url'
|
||||
activeListUrls: ['fake-url']
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -427,7 +428,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined
|
||||
activeListUrls: undefined
|
||||
})
|
||||
store.dispatch(updateVersion())
|
||||
})
|
||||
@ -466,15 +467,7 @@ describe('list reducer', () => {
|
||||
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS)
|
||||
})
|
||||
it('sets selected list', () => {
|
||||
expect(store.getState().selectedListUrl).toEqual(DEFAULT_TOKEN_LIST_URL)
|
||||
})
|
||||
it('default list is initialized', () => {
|
||||
expect(store.getState().byUrl[DEFAULT_TOKEN_LIST_URL]).toEqual({
|
||||
error: null,
|
||||
current: null,
|
||||
loadingRequestId: null,
|
||||
pendingUpdate: null
|
||||
})
|
||||
expect(store.getState().activeListUrls).toEqual(DEFAULT_ACTIVE_LIST_URLS)
|
||||
})
|
||||
})
|
||||
describe('initialized with a different set of lists', () => {
|
||||
@ -494,7 +487,7 @@ describe('list reducer', () => {
|
||||
pendingUpdate: null
|
||||
}
|
||||
},
|
||||
selectedListUrl: undefined,
|
||||
activeListUrls: undefined,
|
||||
lastInitializedDefaultListOfLists: ['https://unpkg.com/@uniswap/default-token-list@latest']
|
||||
})
|
||||
store.dispatch(updateVersion())
|
||||
@ -538,7 +531,7 @@ describe('list reducer', () => {
|
||||
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS)
|
||||
})
|
||||
it('sets default list to selected list', () => {
|
||||
expect(store.getState().selectedListUrl).toEqual(DEFAULT_TOKEN_LIST_URL)
|
||||
expect(store.getState().activeListUrls).toEqual(DEFAULT_ACTIVE_LIST_URLS)
|
||||
})
|
||||
it('default list is initialized', () => {
|
||||
expect(store.getState().byUrl[DEFAULT_TOKEN_LIST_URL]).toEqual({
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { DEFAULT_ACTIVE_LIST_URLS } from './../../constants/lists'
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { getVersionUpgrade, VersionUpgrade } from '@uniswap/token-lists'
|
||||
import { TokenList } from '@uniswap/token-lists/dist/types'
|
||||
import { DEFAULT_LIST_OF_LISTS, DEFAULT_TOKEN_LIST_URL } from '../../constants/lists'
|
||||
import { DEFAULT_LIST_OF_LISTS } from '../../constants/lists'
|
||||
import { updateVersion } from '../global/actions'
|
||||
import { acceptListUpdate, addList, fetchTokenList, removeList, selectList } from './actions'
|
||||
import { acceptListUpdate, addList, fetchTokenList, removeList, enableList, disableList } from './actions'
|
||||
|
||||
export interface ListsState {
|
||||
readonly byUrl: {
|
||||
@ -16,7 +17,9 @@ export interface ListsState {
|
||||
}
|
||||
// this contains the default list of lists from the last time the updateVersion was called, i.e. the app was reloaded
|
||||
readonly lastInitializedDefaultListOfLists?: string[]
|
||||
readonly selectedListUrl: string | undefined
|
||||
|
||||
// currently active lists
|
||||
readonly activeListUrls: string[] | undefined
|
||||
}
|
||||
|
||||
type ListState = ListsState['byUrl'][string]
|
||||
@ -38,7 +41,7 @@ const initialState: ListsState = {
|
||||
return memo
|
||||
}, {})
|
||||
},
|
||||
selectedListUrl: DEFAULT_TOKEN_LIST_URL
|
||||
activeListUrls: DEFAULT_ACTIVE_LIST_URLS
|
||||
}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
@ -59,6 +62,7 @@ export default createReducer(initialState, builder =>
|
||||
// no-op if update does nothing
|
||||
if (current) {
|
||||
const upgradeType = getVersionUpgrade(current.version, tokenList.version)
|
||||
|
||||
if (upgradeType === VersionUpgrade.NONE) return
|
||||
if (loadingRequestId === null || loadingRequestId === requestId) {
|
||||
state.byUrl[url] = {
|
||||
@ -93,13 +97,6 @@ export default createReducer(initialState, builder =>
|
||||
pendingUpdate: null
|
||||
}
|
||||
})
|
||||
.addCase(selectList, (state, { payload: url }) => {
|
||||
state.selectedListUrl = url
|
||||
// automatically adds list
|
||||
if (!state.byUrl[url]) {
|
||||
state.byUrl[url] = NEW_LIST_STATE
|
||||
}
|
||||
})
|
||||
.addCase(addList, (state, { payload: url }) => {
|
||||
if (!state.byUrl[url]) {
|
||||
state.byUrl[url] = NEW_LIST_STATE
|
||||
@ -109,8 +106,27 @@ export default createReducer(initialState, builder =>
|
||||
if (state.byUrl[url]) {
|
||||
delete state.byUrl[url]
|
||||
}
|
||||
if (state.selectedListUrl === url) {
|
||||
state.selectedListUrl = url === DEFAULT_TOKEN_LIST_URL ? Object.keys(state.byUrl)[0] : DEFAULT_TOKEN_LIST_URL
|
||||
// remove list from active urls if needed
|
||||
if (state.activeListUrls && state.activeListUrls.includes(url)) {
|
||||
state.activeListUrls = state.activeListUrls.filter(u => u !== url)
|
||||
}
|
||||
})
|
||||
.addCase(enableList, (state, { payload: url }) => {
|
||||
if (!state.byUrl[url]) {
|
||||
state.byUrl[url] = NEW_LIST_STATE
|
||||
}
|
||||
|
||||
if (state.activeListUrls && !state.activeListUrls.includes(url)) {
|
||||
state.activeListUrls.push(url)
|
||||
}
|
||||
|
||||
if (!state.activeListUrls) {
|
||||
state.activeListUrls = [url]
|
||||
}
|
||||
})
|
||||
.addCase(disableList, (state, { payload: url }) => {
|
||||
if (state.activeListUrls && state.activeListUrls.includes(url)) {
|
||||
state.activeListUrls = state.activeListUrls.filter(u => u !== url)
|
||||
}
|
||||
})
|
||||
.addCase(acceptListUpdate, (state, { payload: url }) => {
|
||||
@ -127,7 +143,7 @@ export default createReducer(initialState, builder =>
|
||||
// state loaded from localStorage, but new lists have never been initialized
|
||||
if (!state.lastInitializedDefaultListOfLists) {
|
||||
state.byUrl = initialState.byUrl
|
||||
state.selectedListUrl = DEFAULT_TOKEN_LIST_URL
|
||||
state.activeListUrls = initialState.activeListUrls
|
||||
} else if (state.lastInitializedDefaultListOfLists) {
|
||||
const lastInitializedSet = state.lastInitializedDefaultListOfLists.reduce<Set<string>>(
|
||||
(s, l) => s.add(l),
|
||||
@ -150,11 +166,17 @@ export default createReducer(initialState, builder =>
|
||||
|
||||
state.lastInitializedDefaultListOfLists = DEFAULT_LIST_OF_LISTS
|
||||
|
||||
if (!state.selectedListUrl) {
|
||||
state.selectedListUrl = DEFAULT_TOKEN_LIST_URL
|
||||
if (!state.byUrl[DEFAULT_TOKEN_LIST_URL]) {
|
||||
state.byUrl[DEFAULT_TOKEN_LIST_URL] = NEW_LIST_STATE
|
||||
}
|
||||
// if no active lists, activate defaults
|
||||
if (!state.activeListUrls) {
|
||||
state.activeListUrls = DEFAULT_ACTIVE_LIST_URLS
|
||||
|
||||
// for each list on default list, initialize if needed
|
||||
DEFAULT_ACTIVE_LIST_URLS.map((listUrl: string) => {
|
||||
if (!state.byUrl[listUrl]) {
|
||||
state.byUrl[listUrl] = NEW_LIST_STATE
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
@ -1,26 +1,30 @@
|
||||
import { useAllLists } from 'state/lists/hooks'
|
||||
import { UNSUPPORTED_LIST_URLS } from './../../constants/lists'
|
||||
import { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
|
||||
import { addPopup } from '../application/actions'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { AppDispatch } from '../index'
|
||||
import { acceptListUpdate } from './actions'
|
||||
import { useActiveListUrls } from './hooks'
|
||||
import { useAllInactiveTokens } from 'hooks/Tokens'
|
||||
|
||||
export default function Updater(): null {
|
||||
const { library } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const selectedListUrl = useSelector<AppState, AppState['lists']['selectedListUrl']>(
|
||||
state => state.lists.selectedListUrl
|
||||
)
|
||||
|
||||
const isWindowVisible = useIsWindowVisible()
|
||||
|
||||
const fetchList = useFetchListCallback()
|
||||
// get all loaded lists, and the active urls
|
||||
const lists = useAllLists()
|
||||
const activeListUrls = useActiveListUrls()
|
||||
|
||||
// initiate loading
|
||||
useAllInactiveTokens()
|
||||
|
||||
const fetchList = useFetchListCallback()
|
||||
const fetchAllListsCallback = useCallback(() => {
|
||||
if (!isWindowVisible) return
|
||||
Object.keys(lists).forEach(url =>
|
||||
@ -35,7 +39,6 @@ export default function Updater(): null {
|
||||
useEffect(() => {
|
||||
Object.keys(lists).forEach(listUrl => {
|
||||
const list = lists[listUrl]
|
||||
|
||||
if (!list.current && !list.loadingRequestId && !list.error) {
|
||||
fetchList(listUrl).catch(error => console.debug('list added fetching error', error))
|
||||
}
|
||||
@ -57,21 +60,6 @@ export default function Updater(): null {
|
||||
// automatically update minor/patch as long as bump matches the min update
|
||||
if (bump >= min) {
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
if (listUrl === selectedListUrl) {
|
||||
dispatch(
|
||||
addPopup({
|
||||
key: listUrl,
|
||||
content: {
|
||||
listUpdate: {
|
||||
listUrl,
|
||||
oldList: list.current,
|
||||
newList: list.pendingUpdate,
|
||||
auto: true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
`List at url ${listUrl} could not automatically update because the version bump was only PATCH/MINOR while the update had breaking changes and should have been MAJOR`
|
||||
@ -80,26 +68,14 @@ export default function Updater(): null {
|
||||
break
|
||||
|
||||
case VersionUpgrade.MAJOR:
|
||||
if (listUrl === selectedListUrl) {
|
||||
dispatch(
|
||||
addPopup({
|
||||
key: listUrl,
|
||||
content: {
|
||||
listUpdate: {
|
||||
listUrl,
|
||||
auto: false,
|
||||
oldList: list.current,
|
||||
newList: list.pendingUpdate
|
||||
}
|
||||
},
|
||||
removeAfterMs: null
|
||||
})
|
||||
)
|
||||
// accept update if list is active or list in background
|
||||
if (activeListUrls?.includes(listUrl) || UNSUPPORTED_LIST_URLS.includes(listUrl)) {
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [dispatch, lists, selectedListUrl])
|
||||
}, [dispatch, lists, activeListUrls])
|
||||
|
||||
return null
|
||||
}
|
||||
|
@ -17,6 +17,33 @@ export function useMintState(): AppState['mint'] {
|
||||
return useSelector<AppState, AppState['mint']>(state => state.mint)
|
||||
}
|
||||
|
||||
export function useMintActionHandlers(
|
||||
noLiquidity: boolean | undefined
|
||||
): {
|
||||
onFieldAInput: (typedValue: string) => void
|
||||
onFieldBInput: (typedValue: string) => void
|
||||
} {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const onFieldAInput = useCallback(
|
||||
(typedValue: string) => {
|
||||
dispatch(typeInput({ field: Field.CURRENCY_A, typedValue, noLiquidity: noLiquidity === true }))
|
||||
},
|
||||
[dispatch, noLiquidity]
|
||||
)
|
||||
const onFieldBInput = useCallback(
|
||||
(typedValue: string) => {
|
||||
dispatch(typeInput({ field: Field.CURRENCY_B, typedValue, noLiquidity: noLiquidity === true }))
|
||||
},
|
||||
[dispatch, noLiquidity]
|
||||
)
|
||||
|
||||
return {
|
||||
onFieldAInput,
|
||||
onFieldBInput
|
||||
}
|
||||
}
|
||||
|
||||
export function useDerivedMintInfo(
|
||||
currencyA: Currency | undefined,
|
||||
currencyB: Currency | undefined
|
||||
@ -167,30 +194,3 @@ export function useDerivedMintInfo(
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
export function useMintActionHandlers(
|
||||
noLiquidity: boolean | undefined
|
||||
): {
|
||||
onFieldAInput: (typedValue: string) => void
|
||||
onFieldBInput: (typedValue: string) => void
|
||||
} {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const onFieldAInput = useCallback(
|
||||
(typedValue: string) => {
|
||||
dispatch(typeInput({ field: Field.CURRENCY_A, typedValue, noLiquidity: noLiquidity === true }))
|
||||
},
|
||||
[dispatch, noLiquidity]
|
||||
)
|
||||
const onFieldBInput = useCallback(
|
||||
(typedValue: string) => {
|
||||
dispatch(typeInput({ field: Field.CURRENCY_B, typedValue, noLiquidity: noLiquidity === true }))
|
||||
},
|
||||
[dispatch, noLiquidity]
|
||||
)
|
||||
|
||||
return {
|
||||
onFieldAInput,
|
||||
onFieldBInput
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,25 @@ import ReactGA from 'react-ga'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { ArrowLeft, X } from 'react-feather'
|
||||
import { ArrowLeft, X, ExternalLink as LinkIconFeather, Trash } from 'react-feather'
|
||||
|
||||
export const ButtonText = styled.button`
|
||||
outline: none;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
export const Button = styled.button.attrs<{ warning: boolean }, { backgroundColor: string }>(({ warning, theme }) => ({
|
||||
backgroundColor: warning ? theme.red1 : theme.primary1
|
||||
@ -39,6 +57,20 @@ export const CloseIcon = styled(X)<{ onClick: () => void }>`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
// for wrapper react feather icons
|
||||
export const IconWrapper = styled.div<{ stroke?: string; size?: string; marginRight?: string; marginLeft?: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: ${({ size }) => size ?? '20px'};
|
||||
height: ${({ size }) => size ?? '20px'};
|
||||
margin-right: ${({ marginRight }) => marginRight ?? 0};
|
||||
margin-left: ${({ marginLeft }) => marginLeft ?? 0};
|
||||
& > * {
|
||||
stroke: ${({ theme, stroke }) => stroke ?? theme.blue1};
|
||||
}
|
||||
`
|
||||
|
||||
// A button that triggers some onClick result, but looks like a link.
|
||||
export const LinkStyledButton = styled.button<{ disabled?: boolean }>`
|
||||
border: none;
|
||||
@ -104,6 +136,51 @@ const StyledLink = styled.a`
|
||||
}
|
||||
`
|
||||
|
||||
const LinkIconWrapper = styled.a`
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export const LinkIcon = styled(LinkIconFeather)`
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
margin-left: 10px;
|
||||
stroke: ${({ theme }) => theme.blue1};
|
||||
`
|
||||
|
||||
export const TrashIcon = styled(Trash)`
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
margin-left: 10px;
|
||||
stroke: ${({ theme }) => theme.text3};
|
||||
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
|
||||
:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
`
|
||||
|
||||
const rotateImg = keyframes`
|
||||
0% {
|
||||
transform: perspective(1000px) rotateY(0deg);
|
||||
@ -149,6 +226,36 @@ export function ExternalLink({
|
||||
return <StyledLink target={target} rel={rel} href={href} onClick={handleClick} {...rest} />
|
||||
}
|
||||
|
||||
export function ExternalLinkIcon({
|
||||
target = '_blank',
|
||||
href,
|
||||
rel = 'noopener noreferrer',
|
||||
...rest
|
||||
}: Omit<HTMLProps<HTMLAnchorElement>, 'as' | 'ref' | 'onClick'> & { href: string }) {
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
// don't prevent default, don't redirect if it's a new tab
|
||||
if (target === '_blank' || event.ctrlKey || event.metaKey) {
|
||||
ReactGA.outboundLink({ label: href }, () => {
|
||||
console.debug('Fired outbound link event', href)
|
||||
})
|
||||
} else {
|
||||
event.preventDefault()
|
||||
// send a ReactGA event and then trigger a location change
|
||||
ReactGA.outboundLink({ label: href }, () => {
|
||||
window.location.href = href
|
||||
})
|
||||
}
|
||||
},
|
||||
[href, target]
|
||||
)
|
||||
return (
|
||||
<LinkIconWrapper target={target} rel={rel} href={href} onClick={handleClick} {...rest}>
|
||||
<LinkIcon />
|
||||
</LinkIconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const rotate = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
|
@ -74,8 +74,9 @@ export function colors(darkMode: boolean): Colors {
|
||||
secondary3: darkMode ? '#17000b26' : '#FDEAF1',
|
||||
|
||||
// other
|
||||
red1: '#FF6871',
|
||||
red1: '#FD4040',
|
||||
red2: '#F82D3A',
|
||||
red3: '#D60000',
|
||||
green1: '#27AE60',
|
||||
yellow1: '#FFE270',
|
||||
yellow2: '#F3841E',
|
||||
@ -156,7 +157,7 @@ export const TYPE = {
|
||||
return <TextWrapper fontWeight={500} fontSize={11} {...props} />
|
||||
},
|
||||
blue(props: TextProps) {
|
||||
return <TextWrapper fontWeight={500} color={'primary1'} {...props} />
|
||||
return <TextWrapper fontWeight={500} color={'blue1'} {...props} />
|
||||
},
|
||||
yellow(props: TextProps) {
|
||||
return <TextWrapper fontWeight={500} color={'yellow1'} {...props} />
|
||||
|
1
src/theme/styled.d.ts
vendored
1
src/theme/styled.d.ts
vendored
@ -40,6 +40,7 @@ export interface Colors {
|
||||
// other
|
||||
red1: Color
|
||||
red2: Color
|
||||
red3: Color
|
||||
green1: Color
|
||||
yellow1: Color
|
||||
yellow2: Color
|
||||
|
12
src/utils/listSort.ts
Normal file
12
src/utils/listSort.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { DEFAULT_LIST_OF_LISTS } from './../constants/lists'
|
||||
|
||||
// use ordering of default list of lists to assign priority
|
||||
export default function sortByListPriority(urlA: string, urlB: string) {
|
||||
const first = DEFAULT_LIST_OF_LISTS.includes(urlA) ? DEFAULT_LIST_OF_LISTS.indexOf(urlA) : Number.MAX_SAFE_INTEGER
|
||||
const second = DEFAULT_LIST_OF_LISTS.includes(urlB) ? DEFAULT_LIST_OF_LISTS.indexOf(urlB) : Number.MAX_SAFE_INTEGER
|
||||
|
||||
// need reverse order to make sure mapping includes top priority last
|
||||
if (first < second) return 1
|
||||
else if (first > second) return -1
|
||||
return 0
|
||||
}
|
@ -10,8 +10,8 @@ const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(BASE_FEE)
|
||||
|
||||
// computes price breakdown for the trade
|
||||
export function computeTradePriceBreakdown(
|
||||
trade?: Trade
|
||||
): { priceImpactWithoutFee?: Percent; realizedLPFee?: CurrencyAmount } {
|
||||
trade?: Trade | null
|
||||
): { priceImpactWithoutFee: Percent | undefined; realizedLPFee: CurrencyAmount | undefined | null } {
|
||||
// for each hop in our trade, take away the x*y=k price impact from 0.3% fees
|
||||
// e.g. for 3 tokens/2 hops: 1 - ((1 - .03) * (1-.03))
|
||||
const realizedLPFee = !trade
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { Trade, currencyEquals, Percent } from '@uniswap/sdk'
|
||||
|
||||
const ZERO_PERCENT = new Percent('0')
|
||||
const ONE_HUNDRED_PERCENT = new Percent('1')
|
||||
import { ZERO_PERCENT, ONE_HUNDRED_PERCENT } from './../constants/index'
|
||||
import { Trade, Percent, currencyEquals } from '@uniswap/sdk'
|
||||
|
||||
// returns whether tradeB is better than tradeA by at least a threshold percentage amount
|
||||
export function isTradeBetter(
|
||||
|
Loading…
Reference in New Issue
Block a user