Commit 267204d9 authored by Ian Lapham's avatar Ian Lapham Committed by GitHub

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: default avatarCallil Capuozzo <callil.capuozzo@gmail.com>
parent 74f50f1b
...@@ -3,18 +3,9 @@ describe('Lists', () => { ...@@ -3,18 +3,9 @@ describe('Lists', () => {
cy.visit('/swap') cy.visit('/swap')
}) })
it('defaults to uniswap list', () => { // @TODO check if default lists are active when we have them
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get('#currency-search-selected-list-name').should('contain', 'Uniswap')
})
it('change list', () => { it('change list', () => {
cy.get('#swap-currency-output .open-currency-select-button').click() cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get('#currency-search-change-list-button').click() cy.get('.list-token-manage-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')
}) })
}) })
...@@ -16,7 +16,7 @@ const Base = styled(RebassButton)<{ ...@@ -16,7 +16,7 @@ const Base = styled(RebassButton)<{
width: ${({ width }) => (width ? width : '100%')}; width: ${({ width }) => (width ? width : '100%')};
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
border-radius: 12px; border-radius: 20px;
border-radius: ${({ borderRadius }) => borderRadius && borderRadius}; border-radius: ${({ borderRadius }) => borderRadius && borderRadius};
outline: none; outline: none;
border: 1px solid transparent; border: 1px solid transparent;
...@@ -53,13 +53,15 @@ export const ButtonPrimary = styled(Base)` ...@@ -53,13 +53,15 @@ export const ButtonPrimary = styled(Base)`
background-color: ${({ theme }) => darken(0.1, theme.primary1)}; background-color: ${({ theme }) => darken(0.1, theme.primary1)};
} }
&:disabled { &:disabled {
background-color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? theme.primary1 : theme.bg3)}; background-color: ${({ theme, altDisabledStyle, disabled }) =>
color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? 'white' : theme.text3)}; altDisabledStyle ? (disabled ? theme.bg3 : theme.primary1) : theme.bg3};
color: ${({ theme, altDisabledStyle, disabled }) =>
altDisabledStyle ? (disabled ? theme.text3 : 'white') : theme.text3};
cursor: auto; cursor: auto;
box-shadow: none; box-shadow: none;
border: 1px solid transparent; border: 1px solid transparent;
outline: none; outline: none;
opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.7' : '1')}; opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.5' : '1')};
} }
` `
...@@ -97,15 +99,13 @@ export const ButtonGray = styled(Base)` ...@@ -97,15 +99,13 @@ export const ButtonGray = styled(Base)`
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
&:focus { &:focus {
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)}; background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg4)};
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)};
} }
&:hover { &:hover {
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)}; background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg4)};
} }
&:active { &:active {
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg2)}; background-color: ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg4)};
background-color: ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg2)};
} }
` `
...@@ -210,10 +210,10 @@ export const ButtonEmpty = styled(Base)` ...@@ -210,10 +210,10 @@ export const ButtonEmpty = styled(Base)`
text-decoration: underline; text-decoration: underline;
} }
&:hover { &:hover {
text-decoration: underline; text-decoration: none;
} }
&:active { &:active {
text-decoration: underline; text-decoration: none;
} }
&:disabled { &:disabled {
opacity: 50%; opacity: 50%;
...@@ -308,6 +308,17 @@ export function ButtonDropdown({ disabled = false, children, ...rest }: { disabl ...@@ -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) { export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
return ( return (
<ButtonOutlined {...rest} disabled={disabled}> <ButtonOutlined {...rest} disabled={disabled}>
......
...@@ -3,8 +3,8 @@ import styled from 'styled-components' ...@@ -3,8 +3,8 @@ import styled from 'styled-components'
import { CardProps, Text } from 'rebass' import { CardProps, Text } from 'rebass'
import { Box } from 'rebass/styled-components' import { Box } from 'rebass/styled-components'
const Card = styled(Box)<{ padding?: string; border?: string; borderRadius?: string }>` const Card = styled(Box)<{ width?: string; padding?: string; border?: string; borderRadius?: string }>`
width: 100%; width: ${({ width }) => width ?? '100%'};
border-radius: 16px; border-radius: 16px;
padding: 1.25rem; padding: 1.25rem;
padding: ${({ padding }) => padding}; padding: ${({ padding }) => padding};
......
import { Currency, Pair } from '@uniswap/sdk' import { Currency, Pair } from '@uniswap/sdk'
import React, { useState, useContext, useCallback } from 'react' import React, { useState, useCallback } from 'react'
import styled, { ThemeContext } from 'styled-components' import styled from 'styled-components'
import { darken } from 'polished' import { darken } from 'polished'
import { useCurrencyBalance } from '../../state/wallet/hooks' import { useCurrencyBalance } from '../../state/wallet/hooks'
import CurrencySearchModal from '../SearchModal/CurrencySearchModal' import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
...@@ -13,6 +13,7 @@ import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg' ...@@ -13,6 +13,7 @@ import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useTheme from '../../hooks/useTheme'
const InputRow = styled.div<{ selected: boolean }>` const InputRow = styled.div<{ selected: boolean }>`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
...@@ -154,7 +155,7 @@ export default function CurrencyInputPanel({ ...@@ -154,7 +155,7 @@ export default function CurrencyInputPanel({
const [modalOpen, setModalOpen] = useState(false) const [modalOpen, setModalOpen] = useState(false)
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined) const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
const theme = useContext(ThemeContext) const theme = useTheme()
const handleDismissSearch = useCallback(() => { const handleDismissSearch = useCallback(() => {
setModalOpen(false) setModalOpen(false)
......
...@@ -22,6 +22,7 @@ const StyledLogo = styled(Logo)<{ size: string }>` ...@@ -22,6 +22,7 @@ const StyledLogo = styled(Logo)<{ size: string }>`
height: ${({ size }) => size}; height: ${({ size }) => size};
border-radius: ${({ size }) => size}; border-radius: ${({ size }) => size};
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
background-color: ${({ theme }) => theme.white};
` `
export default function CurrencyLogo({ export default function CurrencyLogo({
......
...@@ -17,7 +17,7 @@ import { CountUp } from 'use-count-up' ...@@ -17,7 +17,7 @@ import { CountUp } from 'use-count-up'
import { TYPE, ExternalLink } from '../../theme' import { TYPE, ExternalLink } from '../../theme'
import { YellowCard } from '../Card' import { YellowCard } from '../Card'
import Settings from '../Settings' import { Moon, Sun } from 'react-feather'
import Menu from '../Menu' import Menu from '../Menu'
import Row, { RowFixed } from '../Row' import Row, { RowFixed } from '../Row'
...@@ -254,6 +254,35 @@ const StyledExternalLink = styled(ExternalLink).attrs({ ...@@ -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 } = { const NETWORK_LABELS: { [chainId in ChainId]?: string } = {
[ChainId.RINKEBY]: 'Rinkeby', [ChainId.RINKEBY]: 'Rinkeby',
[ChainId.ROPSTEN]: 'Ropsten', [ChainId.ROPSTEN]: 'Ropsten',
...@@ -266,7 +295,8 @@ export default function Header() { ...@@ -266,7 +295,8 @@ export default function Header() {
const { t } = useTranslation() const { t } = useTranslation()
const userEthBalance = useETHBalances(account ? [account] : [])?.[account ?? ''] const userEthBalance = useETHBalances(account ? [account] : [])?.[account ?? '']
const [isDark] = useDarkModeManager() // const [isDark] = useDarkModeManager()
const [darkMode, toggleDarkMode] = useDarkModeManager()
const toggleClaimModal = useToggleSelfClaimModal() const toggleClaimModal = useToggleSelfClaimModal()
...@@ -291,7 +321,7 @@ export default function Header() { ...@@ -291,7 +321,7 @@ export default function Header() {
<HeaderRow> <HeaderRow>
<Title href="."> <Title href=".">
<UniIcon> <UniIcon>
<img width={'24px'} src={isDark ? LogoDark : Logo} alt="logo" /> <img width={'24px'} src={darkMode ? LogoDark : Logo} alt="logo" />
</UniIcon> </UniIcon>
</Title> </Title>
<HeaderLinks> <HeaderLinks>
...@@ -375,7 +405,9 @@ export default function Header() { ...@@ -375,7 +405,9 @@ export default function Header() {
</AccountElement> </AccountElement>
</HeaderElement> </HeaderElement>
<HeaderElementWrap> <HeaderElementWrap>
<Settings /> <StyledMenuButton onClick={() => toggleDarkMode()}>
{darkMode ? <Moon size={20} /> : <Sun size={20} />}
</StyledMenuButton>
<Menu /> <Menu />
</HeaderElementWrap> </HeaderElementWrap>
</HeaderControls> </HeaderControls>
......
...@@ -6,7 +6,11 @@ import { NavLink, Link as HistoryLink } from 'react-router-dom' ...@@ -6,7 +6,11 @@ import { NavLink, Link as HistoryLink } from 'react-router-dom'
import { ArrowLeft } from 'react-feather' import { ArrowLeft } from 'react-feather'
import { RowBetween } from '../Row' 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` const Tabs = styled.div`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
...@@ -69,32 +73,34 @@ export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) { ...@@ -69,32 +73,34 @@ export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) {
export function FindPoolTabs() { export function FindPoolTabs() {
return ( return (
<Tabs> <Tabs>
<RowBetween style={{ padding: '1rem' }}> <RowBetween style={{ padding: '1rem 1rem 0 1rem' }}>
<HistoryLink to="/pool"> <HistoryLink to="/pool">
<StyledArrowLeft /> <StyledArrowLeft />
</HistoryLink> </HistoryLink>
<ActiveText>Import Pool</ActiveText> <ActiveText>Import Pool</ActiveText>
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} /> <Settings />
</RowBetween> </RowBetween>
</Tabs> </Tabs>
) )
} }
export function AddRemoveTabs({ adding, creating }: { adding: boolean; creating: boolean }) { export function AddRemoveTabs({ adding, creating }: { adding: boolean; creating: boolean }) {
// reset states on back
const dispatch = useDispatch<AppDispatch>()
return ( return (
<Tabs> <Tabs>
<RowBetween style={{ padding: '1rem' }}> <RowBetween style={{ padding: '1rem 1rem 0 1rem' }}>
<HistoryLink to="/pool"> <HistoryLink
to="/pool"
onClick={() => {
adding && dispatch(resetMintState())
}}
>
<StyledArrowLeft /> <StyledArrowLeft />
</HistoryLink> </HistoryLink>
<ActiveText>{creating ? 'Create a pair' : adding ? 'Add Liquidity' : 'Remove Liquidity'}</ActiveText> <ActiveText>{creating ? 'Create a pair' : adding ? 'Add Liquidity' : 'Remove Liquidity'}</ActiveText>
<QuestionHelper <Settings />
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.'
}
/>
</RowBetween> </RowBetween>
</Tabs> </Tabs>
) )
......
...@@ -9,10 +9,10 @@ import { useTotalSupply } from '../../data/TotalSupply' ...@@ -9,10 +9,10 @@ import { useTotalSupply } from '../../data/TotalSupply'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useTokenBalance } from '../../state/wallet/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 { currencyId } from '../../utils/currencyId'
import { unwrappedToken } from '../../utils/wrappedCurrency' import { unwrappedToken } from '../../utils/wrappedCurrency'
import { ButtonPrimary, ButtonSecondary, ButtonEmpty, ButtonUNIGradient } from '../Button' import { ButtonPrimary, ButtonSecondary, ButtonEmpty } from '../Button'
import { transparentize } from 'polished' import { transparentize } from 'polished'
import { CardNoise } from '../earn/styled' import { CardNoise } from '../earn/styled'
...@@ -202,18 +202,7 @@ export default function FullPositionCard({ pair, border, stakedBalance }: Positi ...@@ -202,18 +202,7 @@ export default function FullPositionCard({ pair, border, stakedBalance }: Positi
<Text fontWeight={500} fontSize={20}> <Text fontWeight={500} fontSize={20}>
{!currency0 || !currency1 ? <Dots>Loading</Dots> : `${currency0.symbol}/${currency1.symbol}`} {!currency0 || !currency1 ? <Dots>Loading</Dots> : `${currency0.symbol}/${currency1.symbol}`}
</Text> </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> </AutoRow>
<RowFixed gap="8px"> <RowFixed gap="8px">
<ButtonEmpty <ButtonEmpty
padding="6px 8px" padding="6px 8px"
......
import styled from 'styled-components' import styled from 'styled-components'
import { Box } from 'rebass/styled-components' import { Box } from 'rebass/styled-components'
const Row = styled(Box)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>` const Row = styled(Box)<{
width: 100%; width?: string
align?: string
justify?: string
padding?: string
border?: string
borderRadius?: string
}>`
width: ${({ width }) => width ?? '100%'};
display: flex; display: flex;
padding: 0; padding: 0;
align-items: ${({ align }) => (align ? align : 'center')}; align-items: ${({ align }) => align ?? 'center'};
justify-content: ${({ justify }) => justify ?? 'flex-start'};
padding: ${({ padding }) => padding}; padding: ${({ padding }) => padding};
border: ${({ border }) => border}; border: ${({ border }) => border};
border-radius: ${({ borderRadius }) => borderRadius}; border-radius: ${({ borderRadius }) => borderRadius};
......
...@@ -4,18 +4,19 @@ import { FixedSizeList } from 'react-window' ...@@ -4,18 +4,19 @@ import { FixedSizeList } from 'react-window'
import { Text } from 'rebass' import { Text } from 'rebass'
import styled from 'styled-components' import styled from 'styled-components'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useSelectedTokenList, WrappedTokenInfo } from '../../state/lists/hooks' import { WrappedTokenInfo, useCombinedActiveList } from '../../state/lists/hooks'
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
import { useCurrencyBalance } from '../../state/wallet/hooks' import { useCurrencyBalance } from '../../state/wallet/hooks'
import { LinkStyledButton, TYPE } from '../../theme' import { TYPE } from '../../theme'
import { useIsUserAddedToken } from '../../hooks/Tokens' import { useIsUserAddedToken, useAllInactiveTokens } from '../../hooks/Tokens'
import Column from '../Column' import Column from '../Column'
import { RowFixed } from '../Row' import { RowFixed } from '../Row'
import CurrencyLogo from '../CurrencyLogo' import CurrencyLogo from '../CurrencyLogo'
import { MouseoverTooltip } from '../Tooltip' import { MouseoverTooltip } from '../Tooltip'
import { FadedSpan, MenuItem } from './styleds' import { MenuItem } from './styleds'
import Loader from '../Loader' import Loader from '../Loader'
import { isTokenOnList } from '../../utils' import { isTokenOnList } from '../../utils'
import ImportRow from './ImportRow'
import { wrappedCurrency } from 'utils/wrappedCurrency'
function currencyKey(currency: Currency): string { function currencyKey(currency: Currency): string {
return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : '' return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : ''
...@@ -93,16 +94,13 @@ function CurrencyRow({ ...@@ -93,16 +94,13 @@ function CurrencyRow({
otherSelected: boolean otherSelected: boolean
style: CSSProperties style: CSSProperties
}) { }) {
const { account, chainId } = useActiveWeb3React() const { account } = useActiveWeb3React()
const key = currencyKey(currency) const key = currencyKey(currency)
const selectedTokenList = useSelectedTokenList() const selectedTokenList = useCombinedActiveList()
const isOnSelectedList = isTokenOnList(selectedTokenList, currency) const isOnSelectedList = isTokenOnList(selectedTokenList, currency)
const customAdded = useIsUserAddedToken(currency) const customAdded = useIsUserAddedToken(currency)
const balance = useCurrencyBalance(account ?? undefined, currency) const balance = useCurrencyBalance(account ?? undefined, currency)
const removeToken = useRemoveUserAddedToken()
const addToken = useAddUserToken()
// only show add or remove buttons if not on selected list // only show add or remove buttons if not on selected list
return ( return (
<MenuItem <MenuItem
...@@ -117,34 +115,9 @@ function CurrencyRow({ ...@@ -117,34 +115,9 @@ function CurrencyRow({
<Text title={currency.name} fontWeight={500}> <Text title={currency.name} fontWeight={500}>
{currency.symbol} {currency.symbol}
</Text> </Text>
<FadedSpan> <TYPE.darkGray ml="0px" fontSize={'12px'} fontWeight={300}>
{!isOnSelectedList && customAdded ? ( {currency.name} {!isOnSelectedList && customAdded && '• Added by user'}
<TYPE.main fontWeight={500}> </TYPE.darkGray>
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>
</Column> </Column>
<TokenTags currency={currency} /> <TokenTags currency={currency} />
<RowFixed style={{ justifySelf: 'flex-end' }}> <RowFixed style={{ justifySelf: 'flex-end' }}>
...@@ -161,7 +134,9 @@ export default function CurrencyList({ ...@@ -161,7 +134,9 @@ export default function CurrencyList({
onCurrencySelect, onCurrencySelect,
otherCurrency, otherCurrency,
fixedListRef, fixedListRef,
showETH showETH,
showImportView,
setImportToken
}: { }: {
height: number height: number
currencies: Currency[] currencies: Currency[]
...@@ -170,15 +145,39 @@ export default function CurrencyList({ ...@@ -170,15 +145,39 @@ export default function CurrencyList({
otherCurrency?: Currency | null otherCurrency?: Currency | null
fixedListRef?: MutableRefObject<FixedSizeList | undefined> fixedListRef?: MutableRefObject<FixedSizeList | undefined>
showETH: boolean showETH: boolean
showImportView: () => void
setImportToken: (token: Token) => void
}) { }) {
const itemData = useMemo(() => (showETH ? [Currency.ETHER, ...currencies] : currencies), [currencies, showETH]) const itemData = useMemo(() => (showETH ? [Currency.ETHER, ...currencies] : currencies), [currencies, showETH])
const { chainId } = useActiveWeb3React()
const inactiveTokens: {
[address: string]: Token
} = useAllInactiveTokens()
const Row = useCallback( const Row = useCallback(
({ data, index, style }) => { ({ data, index, style }) => {
const currency: Currency = data[index] const currency: Currency = data[index]
const isSelected = Boolean(selectedCurrency && currencyEquals(selectedCurrency, currency)) const isSelected = Boolean(selectedCurrency && currencyEquals(selectedCurrency, currency))
const otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency)) const otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency))
const handleSelect = () => onCurrencySelect(currency) const handleSelect = () => onCurrencySelect(currency)
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 ( return (
<CurrencyRow <CurrencyRow
style={style} style={style}
...@@ -188,8 +187,9 @@ export default function CurrencyList({ ...@@ -188,8 +187,9 @@ export default function CurrencyList({
otherSelected={otherSelected} otherSelected={otherSelected}
/> />
) )
}
}, },
[onCurrencySelect, otherCurrency, selectedCurrency] [chainId, inactiveTokens, onCurrencySelect, otherCurrency, selectedCurrency, setImportToken, showImportView]
) )
const itemKey = useCallback((index: number, data: any) => currencyKey(data[index]), []) const itemKey = useCallback((index: number, data: any) => currencyKey(data[index]), [])
......
import { Currency } from '@uniswap/sdk' import { Currency, Token } from '@uniswap/sdk'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import ReactGA from 'react-ga'
import useLast from '../../hooks/useLast' import useLast from '../../hooks/useLast'
import Modal from '../Modal' import Modal from '../Modal'
import { CurrencySearch } from './CurrencySearch' 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 { interface CurrencySearchModalProps {
isOpen: boolean isOpen: boolean
...@@ -15,6 +18,13 @@ interface CurrencySearchModalProps { ...@@ -15,6 +18,13 @@ interface CurrencySearchModalProps {
showCommonBases?: boolean showCommonBases?: boolean
} }
export enum CurrencyModalView {
search,
manage,
importToken,
importList
}
export default function CurrencySearchModal({ export default function CurrencySearchModal({
isOpen, isOpen,
onDismiss, onDismiss,
...@@ -23,12 +33,12 @@ export default function CurrencySearchModal({ ...@@ -23,12 +33,12 @@ export default function CurrencySearchModal({
otherSelectedCurrency, otherSelectedCurrency,
showCommonBases = false showCommonBases = false
}: CurrencySearchModalProps) { }: CurrencySearchModalProps) {
const [listView, setListView] = useState<boolean>(false) const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.manage)
const lastOpen = useLast(isOpen) const lastOpen = useLast(isOpen)
useEffect(() => { useEffect(() => {
if (isOpen && !lastOpen) { if (isOpen && !lastOpen) {
setListView(false) setModalView(CurrencyModalView.search)
} }
}, [isOpen, lastOpen]) }, [isOpen, lastOpen])
...@@ -40,35 +50,54 @@ export default function CurrencySearchModal({ ...@@ -40,35 +50,54 @@ export default function CurrencySearchModal({
[onDismiss, onCurrencySelect] [onDismiss, onCurrencySelect]
) )
const handleClickChangeList = useCallback(() => { // for token import view
ReactGA.event({ const prevView = usePrevious(modalView)
category: 'Lists',
action: 'Change Lists' // used for import token flow
}) const [importToken, setImportToken] = useState<Token | undefined>()
setListView(true)
}, []) // used for import list
const handleClickBack = useCallback(() => { const [importList, setImportList] = useState<TokenList | undefined>()
ReactGA.event({ const [listURL, setListUrl] = useState<string | undefined>()
category: 'Lists',
action: 'Back' // change min height if not searching
}) const minHeight = modalView === CurrencyModalView.importToken || modalView === CurrencyModalView.importList ? 40 : 80
setListView(false)
}, [])
return ( return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={80} minHeight={listView ? 40 : 80}> <Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={80} minHeight={minHeight}>
{listView ? ( {modalView === CurrencyModalView.search ? (
<ListSelect onDismiss={onDismiss} onBack={handleClickBack} />
) : (
<CurrencySearch <CurrencySearch
isOpen={isOpen} isOpen={isOpen}
onDismiss={onDismiss} onDismiss={onDismiss}
onCurrencySelect={handleCurrencySelect} onCurrencySelect={handleCurrencySelect}
onChangeList={handleClickChangeList}
selectedCurrency={selectedCurrency} selectedCurrency={selectedCurrency}
otherSelectedCurrency={otherSelectedCurrency} otherSelectedCurrency={otherSelectedCurrency}
showCommonBases={showCommonBases} 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> </Modal>
) )
......
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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[] { ...@@ -30,7 +30,15 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
return tokens.filter(token => { return tokens.filter(token => {
const { symbol, name } = token const { symbol, name } = token
return (symbol && matchesSearch(symbol)) || (name && matchesSearch(name)) 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` ...@@ -11,15 +11,53 @@ export const ModalInfo = styled.div`
flex: 1; flex: 1;
user-select: none; 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)` export const FadedSpan = styled(RowFixed)`
color: ${({ theme }) => theme.primary1}; color: ${({ theme }) => theme.primary1};
font-size: 14px; font-size: 14px;
` `
export const Checkbox = styled.input`
border: 1px solid ${({ theme }) => theme.red3};
height: 20px;
margin: 0;
`
export const PaddedColumn = styled(AutoColumn)` export const PaddedColumn = styled(AutoColumn)`
padding: 20px; padding: 20px;
padding-bottom: 12px;
` `
export const MenuItem = styled(RowBetween)` export const MenuItem = styled(RowBetween)`
......
...@@ -6,7 +6,6 @@ import { useOnClickOutside } from '../../hooks/useOnClickOutside' ...@@ -6,7 +6,6 @@ import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import { ApplicationModal } from '../../state/application/actions' import { ApplicationModal } from '../../state/application/actions'
import { useModalOpen, useToggleSettingsMenu } from '../../state/application/hooks' import { useModalOpen, useToggleSettingsMenu } from '../../state/application/hooks'
import { import {
useDarkModeManager,
useExpertModeManager, useExpertModeManager,
useUserTransactionTTL, useUserTransactionTTL,
useUserSlippageTolerance, useUserSlippageTolerance,
...@@ -26,7 +25,7 @@ const StyledMenuIcon = styled(Settings)` ...@@ -26,7 +25,7 @@ const StyledMenuIcon = styled(Settings)`
width: 20px; width: 20px;
> * { > * {
stroke: ${({ theme }) => theme.text1}; stroke: ${({ theme }) => theme.text3};
} }
` `
...@@ -51,7 +50,7 @@ const StyledMenuButton = styled.button` ...@@ -51,7 +50,7 @@ const StyledMenuButton = styled.button`
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 35px; height: 35px;
background-color: ${({ theme }) => theme.bg3}; /* background-color: ${({ theme }) => theme.bg3}; */
padding: 0.15rem 0.5rem; padding: 0.15rem 0.5rem;
border-radius: 0.5rem; border-radius: 0.5rem;
...@@ -60,7 +59,7 @@ const StyledMenuButton = styled.button` ...@@ -60,7 +59,7 @@ const StyledMenuButton = styled.button`
:focus { :focus {
cursor: pointer; cursor: pointer;
outline: none; outline: none;
background-color: ${({ theme }) => theme.bg4}; /* background-color: ${({ theme }) => theme.bg4}; */
} }
svg { svg {
...@@ -94,18 +93,12 @@ const MenuFlyout = styled.span` ...@@ -94,18 +93,12 @@ const MenuFlyout = styled.span`
flex-direction: column; flex-direction: column;
font-size: 1rem; font-size: 1rem;
position: absolute; position: absolute;
top: 4rem; top: 2rem;
right: 0rem; right: 0rem;
z-index: 100; z-index: 100;
${({ theme }) => theme.mediaWidth.upToExtraSmall`
min-width: 18.125rem;
right: -46px;
`};
${({ theme }) => theme.mediaWidth.upToMedium` ${({ theme }) => theme.mediaWidth.upToMedium`
min-width: 18.125rem; min-width: 18.125rem;
top: -22rem;
`}; `};
` `
...@@ -138,8 +131,6 @@ export default function SettingsTab() { ...@@ -138,8 +131,6 @@ export default function SettingsTab() {
const [singleHopOnly, setSingleHopOnly] = useUserSingleHopOnly() const [singleHopOnly, setSingleHopOnly] = useUserSingleHopOnly()
const [darkMode, toggleDarkMode] = useDarkModeManager()
// show confirmation view before turning on // show confirmation view before turning on
const [showConfirmation, setShowConfirmation] = useState(false) const [showConfirmation, setShowConfirmation] = useState(false)
...@@ -246,14 +237,6 @@ export default function SettingsTab() { ...@@ -246,14 +237,6 @@ export default function SettingsTab() {
toggle={() => (singleHopOnly ? setSingleHopOnly(false) : setSingleHopOnly(true))} toggle={() => (singleHopOnly ? setSingleHopOnly(false) : setSingleHopOnly(true))}
/> />
</RowBetween> </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> </AutoColumn>
</MenuFlyout> </MenuFlyout>
)} )}
......
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 }>` ...@@ -26,14 +26,12 @@ const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>` const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
border-radius: 12px; border-radius: 12px;
border: none; border: none;
/* border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)}; */
background: ${({ theme }) => theme.bg3}; background: ${({ theme }) => theme.bg3};
display: flex; display: flex;
width: fit-content; width: fit-content;
cursor: pointer; cursor: pointer;
outline: none; outline: none;
padding: 0; padding: 0;
/* background-color: transparent; */
` `
export interface ToggleProps { export interface ToggleProps {
......
...@@ -113,7 +113,7 @@ export default function TokenWarningModal({ ...@@ -113,7 +113,7 @@ export default function TokenWarningModal({
when interacting with arbitrary ERC20 tokens. when interacting with arbitrary ERC20 tokens.
</TYPE.body> </TYPE.body>
<TYPE.body color={'red2'}> <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> </TYPE.body>
{tokens.map(token => { {tokens.map(token => {
return <TokenWarningCard key={token.address} token={token} /> return <TokenWarningCard key={token.address} token={token} />
......
...@@ -9,7 +9,6 @@ import { AutoColumn } from '../Column' ...@@ -9,7 +9,6 @@ import { AutoColumn } from '../Column'
import QuestionHelper from '../QuestionHelper' import QuestionHelper from '../QuestionHelper'
import { RowBetween, RowFixed } from '../Row' import { RowBetween, RowFixed } from '../Row'
import FormattedPriceImpact from './FormattedPriceImpact' import FormattedPriceImpact from './FormattedPriceImpact'
import { SectionBreak } from './styleds'
import SwapRoute from './SwapRoute' import SwapRoute from './SwapRoute'
const InfoLink = styled(ExternalLink)` const InfoLink = styled(ExternalLink)`
...@@ -30,7 +29,7 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag ...@@ -30,7 +29,7 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
return ( return (
<> <>
<AutoColumn style={{ padding: '0 20px' }}> <AutoColumn style={{ padding: '0 16px' }}>
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
...@@ -86,29 +85,33 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) { ...@@ -86,29 +85,33 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
const showRoute = Boolean(trade && trade.route.path.length > 2) const showRoute = Boolean(trade && trade.route.path.length > 2)
return ( return (
<AutoColumn gap="md"> <AutoColumn gap="0px">
{trade && ( {trade && (
<> <>
<TradeSummary trade={trade} allowedSlippage={allowedSlippage} /> <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />
{showRoute && ( {showRoute && (
<> <>
<SectionBreak /> <RowBetween style={{ padding: '0 16px' }}>
<AutoColumn style={{ padding: '0 24px' }}> <span style={{ display: 'flex', alignItems: 'center' }}>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Route Route
</TYPE.black> </TYPE.black>
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." /> <QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
</RowFixed> </span>
<SwapRoute trade={trade} /> <SwapRoute trade={trade} />
</AutoColumn> </RowBetween>
</> </>
)} )}
<AutoColumn style={{ padding: '0 24px' }}> {!showRoute && (
<InfoLink href={'https://uniswap.info/pair/' + trade.route.pairs[0].liquidityToken.address} target="_blank"> <AutoColumn style={{ padding: '12px 16px 0 16px' }}>
<InfoLink
href={'https://uniswap.info/pair/' + trade.route.pairs[0].liquidityToken.address}
target="_blank"
>
View pair analytics ↗ View pair analytics ↗
</InfoLink> </InfoLink>
</AutoColumn> </AutoColumn>
)}
</> </>
)} )}
</AutoColumn> </AutoColumn>
......
...@@ -5,7 +5,7 @@ import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDet ...@@ -5,7 +5,7 @@ import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDet
const AdvancedDetailsFooter = styled.div<{ show: boolean }>` const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
padding-top: calc(16px + 2rem); padding-top: calc(16px + 2rem);
padding-bottom: 20px; padding-bottom: 16px;
margin-top: -2rem; margin-top: -2rem;
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
......
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' ...@@ -4,32 +4,23 @@ import { ChevronRight } from 'react-feather'
import { Flex } from 'rebass' import { Flex } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import CurrencyLogo from '../CurrencyLogo' import { unwrappedToken } from 'utils/wrappedCurrency'
export default memo(function SwapRoute({ trade }: { trade: Trade }) { export default memo(function SwapRoute({ trade }: { trade: Trade }) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
return ( return (
<Flex <Flex flexWrap="wrap" width="100%" justifyContent="flex-end" alignItems="center">
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"
>
{trade.route.path.map((token, i, path) => { {trade.route.path.map((token, i, path) => {
const isLastItem: boolean = i === path.length - 1 const isLastItem: boolean = i === path.length - 1
const currency = unwrappedToken(token)
return ( return (
<Fragment key={i}> <Fragment key={i}>
<Flex my="0.5rem" alignItems="center" style={{ flexShrink: 0 }}> <Flex alignItems="end">
<CurrencyLogo currency={token} size="1.5rem" /> <TYPE.black fontSize={14} color={theme.text1} ml="0.125rem" mr="0.125rem">
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem"> {currency.symbol}
{token.symbol}
</TYPE.black> </TYPE.black>
</Flex> </Flex>
{isLastItem ? null : <ChevronRight color={theme.text2} />} {isLastItem ? null : <ChevronRight size={12} color={theme.text2} />}
</Fragment> </Fragment>
) )
})} })}
......
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' ...@@ -7,6 +7,7 @@ import { AutoColumn } from '../Column'
export const Wrapper = styled.div` export const Wrapper = styled.div`
position: relative; position: relative;
padding: 1rem;
` `
export const ArrowWrapper = styled.div<{ clickable: boolean }>` export const ArrowWrapper = styled.div<{ clickable: boolean }>`
...@@ -145,3 +146,8 @@ export const SwapShowAcceptChanges = styled(AutoColumn)` ...@@ -145,3 +146,8 @@ export const SwapShowAcceptChanges = styled(AutoColumn)`
border-radius: 12px; border-radius: 12px;
margin-top: 8px; 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( ...@@ -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 // 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 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 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 // SDN OFAC addresses
export const BLOCKED_ADDRESSES: string[] = [ export const BLOCKED_ADDRESSES: string[] = [
'0x7F367cC41522cE07553e823bf3be79A889DEbe1B', '0x7F367cC41522cE07553e823bf3be79A889DEbe1B',
......
// the Uniswap Default token list lives here // the Uniswap Default token list lives here
export const DEFAULT_TOKEN_LIST_URL = 'tokens.uniswap.eth' 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[] = [ export const DEFAULT_LIST_OF_LISTS: string[] = [
DEFAULT_TOKEN_LIST_URL, COMPOUND_LIST,
't2crtokens.eth', // kleros AAVE_LIST,
'tokens.1inch.eth', // 1inch SYNTHETIX_LIST,
'synths.snx.eth', UMA_LIST,
'tokenlist.dharma.eth', WRAPPED_LIST,
'defi.cmc.eth', SET_LIST,
'erc20.cmc.eth', OPYN_LIST,
'stablecoin.cmc.eth', ROLL_LIST,
'tokenlist.zerion.eth', COINGECKO_LIST,
'tokenlist.aave.eth', CMC_ALL_LIST,
'https://tokens.coingecko.com/uniswap/all.json', CMC_STABLECOIN,
'https://app.tryroll.com/tokens.json', KLEROS_LIST,
'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json', ...UNSUPPORTED_LIST_URLS // need to load unsupported tokens as well
'https://defiprime.com/defiprime.tokenlist.json',
'https://umaproject.org/uma.tokenlist.json'
] ]
// default lists to be 'active' aka searched across
export const DEFAULT_ACTIVE_LIST_URLS: string[] = [DEFAULT_TOKEN_LIST_URL]
This diff is collapsed.
{
"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"
}
]
}
import { TokenAddressMap, useDefaultTokenList, useUnsupportedTokenList } from './../state/lists/hooks'
import { parseBytes32String } from '@ethersproject/strings' import { parseBytes32String } from '@ethersproject/strings'
import { Currency, ETHER, Token, currencyEquals } from '@uniswap/sdk' import { Currency, ETHER, Token, currencyEquals } from '@uniswap/sdk'
import { useMemo } from 'react' 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 { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useUserAddedTokens } from '../state/user/hooks' import { useUserAddedTokens } from '../state/user/hooks'
import { isAddress } from '../utils' import { isAddress } from '../utils'
import { useActiveWeb3React } from './index' import { useActiveWeb3React } from './index'
import { useBytes32TokenContract, useTokenContract } from './useContract' import { useBytes32TokenContract, useTokenContract } from './useContract'
import { filterTokens } from '../components/SearchModal/filtering'
import { arrayify } from 'ethers/lib/utils' 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 { chainId } = useActiveWeb3React()
const userAddedTokens = useUserAddedTokens() const userAddedTokens = useUserAddedTokens()
const allTokens = useSelectedTokenList()
return useMemo(() => { return useMemo(() => {
if (!chainId) return {} if (!chainId) return {}
// 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 ( return (
userAddedTokens userAddedTokens
// reduce into all ALL_TOKENS filtered by the current chain // reduce into all ALL_TOKENS filtered by the current chain
...@@ -27,15 +37,82 @@ export function useAllTokens(): { [address: string]: Token } { ...@@ -27,15 +37,82 @@ export function useAllTokens(): { [address: string]: Token } {
}, },
// must make a copy because reduce modifies the map, and we do not // must make a copy because reduce modifies the map, and we do not
// want to make a copy in every iteration // want to make a copy in every iteration
{ ...allTokens[chainId] } { ...mapWithoutUrls }
) )
) )
}, [chainId, userAddedTokens, allTokens]) }
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 // 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() const userAddedTokens = useUserAddedTokens()
if (!currency) {
return false
}
return !!userAddedTokens.find(token => currencyEquals(currency, token)) return !!userAddedTokens.find(token => currencyEquals(currency, token))
} }
......
...@@ -8,6 +8,7 @@ import { PairState, usePairs } from '../data/Reserves' ...@@ -8,6 +8,7 @@ import { PairState, usePairs } from '../data/Reserves'
import { wrappedCurrency } from '../utils/wrappedCurrency' import { wrappedCurrency } from '../utils/wrappedCurrency'
import { useActiveWeb3React } from './index' import { useActiveWeb3React } from './index'
import { useUnsupportedTokens } from './Tokens'
import { useUserSingleHopOnly } from 'state/user/hooks' import { useUserSingleHopOnly } from 'state/user/hooks'
function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] { function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
...@@ -147,3 +148,23 @@ export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: Curr ...@@ -147,3 +148,23 @@ export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: Curr
return null return null
}, [currencyIn, currencyAmountOut, allowedPairs, singleHopOnly]) }, [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' ...@@ -3,6 +3,7 @@ import { shade } from 'polished'
import Vibrant from 'node-vibrant' import Vibrant from 'node-vibrant'
import { hex } from 'wcag-contrast' import { hex } from 'wcag-contrast'
import { Token, ChainId } from '@uniswap/sdk' import { Token, ChainId } from '@uniswap/sdk'
import uriToHttp from 'utils/uriToHttp'
async function getColorFromToken(token: Token): Promise<string | null> { async function getColorFromToken(token: Token): Promise<string | null> {
if (token.chainId === ChainId.RINKEBY && token.address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') { if (token.chainId === ChainId.RINKEBY && token.address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
...@@ -28,6 +29,20 @@ async function getColorFromToken(token: Token): Promise<string | null> { ...@@ -28,6 +29,20 @@ async function getColorFromToken(token: Token): Promise<string | null> {
.catch(() => 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) { export function useColor(token?: Token) {
const [color, setColor] = useState('#2172E5') const [color, setColor] = useState('#2172E5')
...@@ -50,3 +65,26 @@ export function useColor(token?: Token) { ...@@ -50,3 +65,26 @@ export function useColor(token?: Token) {
return color 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' ...@@ -10,7 +10,7 @@ import getTokenList from '../utils/getTokenList'
import resolveENSContentHash from '../utils/resolveENSContentHash' import resolveENSContentHash from '../utils/resolveENSContentHash'
import { useActiveWeb3React } from './index' 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 { chainId, library } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>() const dispatch = useDispatch<AppDispatch>()
...@@ -30,18 +30,19 @@ export function useFetchListCallback(): (listUrl: string) => Promise<TokenList> ...@@ -30,18 +30,19 @@ export function useFetchListCallback(): (listUrl: string) => Promise<TokenList>
[chainId, library] [chainId, library]
) )
// note: prevent dispatch if using for list search or unsupported list
return useCallback( return useCallback(
async (listUrl: string) => { async (listUrl: string, sendDispatch = true) => {
const requestId = nanoid() const requestId = nanoid()
dispatch(fetchTokenList.pending({ requestId, url: listUrl })) sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
return getTokenList(listUrl, ensResolver) return getTokenList(listUrl, ensResolver)
.then(tokenList => { .then(tokenList => {
dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId })) sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
return tokenList return tokenList
}) })
.catch(error => { .catch(error => {
console.debug(`Failed to get list at url ${listUrl}`, 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 throw error
}) })
}, },
......
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' ...@@ -38,6 +38,8 @@ import { Dots, Wrapper } from '../Pool/styleds'
import { ConfirmAddModalBottom } from './ConfirmAddModalBottom' import { ConfirmAddModalBottom } from './ConfirmAddModalBottom'
import { currencyId } from '../../utils/currencyId' import { currencyId } from '../../utils/currencyId'
import { PoolPriceBar } from './PoolPriceBar' import { PoolPriceBar } from './PoolPriceBar'
import { useIsTransactionUnsupported } from 'hooks/Trades'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
export default function AddLiquidity({ export default function AddLiquidity({
match: { match: {
...@@ -76,6 +78,7 @@ export default function AddLiquidity({ ...@@ -76,6 +78,7 @@ export default function AddLiquidity({
poolTokenPercentage, poolTokenPercentage,
error error
} = useDerivedMintInfo(currencyA ?? undefined, currencyB ?? undefined) } = useDerivedMintInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onFieldAInput, onFieldBInput } = useMintActionHandlers(noLiquidity) const { onFieldAInput, onFieldBInput } = useMintActionHandlers(noLiquidity)
const isValid = !error const isValid = !error
...@@ -304,6 +307,8 @@ export default function AddLiquidity({ ...@@ -304,6 +307,8 @@ export default function AddLiquidity({
const isCreate = history.location.pathname.includes('/create') const isCreate = history.location.pathname.includes('/create')
const addIsUnsupported = useIsTransactionUnsupported(currencies?.CURRENCY_A, currencies?.CURRENCY_B)
return ( return (
<> <>
<AppBody> <AppBody>
...@@ -326,7 +331,7 @@ export default function AddLiquidity({ ...@@ -326,7 +331,7 @@ export default function AddLiquidity({
/> />
<AutoColumn gap="20px"> <AutoColumn gap="20px">
{noLiquidity || {noLiquidity ||
(isCreate && ( (isCreate ? (
<ColumnCenter> <ColumnCenter>
<BlueCard> <BlueCard>
<AutoColumn gap="10px"> <AutoColumn gap="10px">
...@@ -342,6 +347,18 @@ export default function AddLiquidity({ ...@@ -342,6 +347,18 @@ export default function AddLiquidity({
</AutoColumn> </AutoColumn>
</BlueCard> </BlueCard>
</ColumnCenter> </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 <CurrencyInputPanel
value={formattedAmounts[Field.CURRENCY_A]} value={formattedAmounts[Field.CURRENCY_A]}
...@@ -390,7 +407,11 @@ export default function AddLiquidity({ ...@@ -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> <ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : ( ) : (
<AutoColumn gap={'md'}> <AutoColumn gap={'md'}>
...@@ -444,12 +465,18 @@ export default function AddLiquidity({ ...@@ -444,12 +465,18 @@ export default function AddLiquidity({
</AutoColumn> </AutoColumn>
</Wrapper> </Wrapper>
</AppBody> </AppBody>
{!addIsUnsupported ? (
{pair && !noLiquidity && pairState !== PairState.INVALID ? ( pair && !noLiquidity && pairState !== PairState.INVALID ? (
<AutoColumn style={{ minWidth: '20rem', width: '100%', maxWidth: '400px', marginTop: '1rem' }}> <AutoColumn style={{ minWidth: '20rem', width: '100%', maxWidth: '400px', marginTop: '1rem' }}>
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} /> <MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn> </AutoColumn>
) : null} ) : null
) : (
<UnsupportedCurrencyFooter
show={addIsUnsupported}
currencies={[currencies.CURRENCY_A, currencies.CURRENCY_B]}
/>
)}
</> </>
) )
} }
...@@ -28,7 +28,6 @@ import RemoveLiquidity from './RemoveLiquidity' ...@@ -28,7 +28,6 @@ import RemoveLiquidity from './RemoveLiquidity'
import { RedirectOldRemoveLiquidityPathStructure } from './RemoveLiquidity/redirects' import { RedirectOldRemoveLiquidityPathStructure } from './RemoveLiquidity/redirects'
import Swap from './Swap' import Swap from './Swap'
import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects' import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
import Vote from './Vote' import Vote from './Vote'
import VotePage from './Vote/VotePage' import VotePage from './Vote/VotePage'
......
...@@ -9,7 +9,7 @@ export const BodyWrapper = styled.div` ...@@ -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), 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); 0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 30px; border-radius: 30px;
padding: 1rem; /* padding: 1rem; */
` `
/** /**
......
...@@ -7,7 +7,6 @@ import { SearchInput } from '../../components/SearchModal/styleds' ...@@ -7,7 +7,6 @@ import { SearchInput } from '../../components/SearchModal/styleds'
import { useAllTokenV1Exchanges } from '../../data/V1' import { useAllTokenV1Exchanges } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useAllTokens, useToken } from '../../hooks/Tokens' import { useAllTokens, useToken } from '../../hooks/Tokens'
import { useSelectedTokenList } from '../../state/lists/hooks'
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks' import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
import { BackArrow, TYPE } from '../../theme' import { BackArrow, TYPE } from '../../theme'
import { LightCard } from '../../components/Card' import { LightCard } from '../../components/Card'
...@@ -18,6 +17,7 @@ import QuestionHelper from '../../components/QuestionHelper' ...@@ -18,6 +17,7 @@ import QuestionHelper from '../../components/QuestionHelper'
import { Dots } from '../../components/swap/styleds' import { Dots } from '../../components/swap/styleds'
import { useAddUserToken } from '../../state/user/hooks' import { useAddUserToken } from '../../state/user/hooks'
import { isTokenOnList } from '../../utils' import { isTokenOnList } from '../../utils'
import { useCombinedActiveList } from '../../state/lists/hooks'
export default function MigrateV1() { export default function MigrateV1() {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
...@@ -28,7 +28,7 @@ export default function MigrateV1() { ...@@ -28,7 +28,7 @@ export default function MigrateV1() {
// automatically add the search token // automatically add the search token
const token = useToken(tokenSearch) const token = useToken(tokenSearch)
const selectedTokenListTokens = useSelectedTokenList() const selectedTokenListTokens = useCombinedActiveList()
const isOnSelectedList = isTokenOnList(selectedTokenListTokens, token ?? undefined) const isOnSelectedList = isTokenOnList(selectedTokenListTokens, token ?? undefined)
const allTokens = useAllTokens() const allTokens = useAllTokens()
const addToken = useAddUserToken() const addToken = useAddUserToken()
......
...@@ -165,7 +165,13 @@ export default function Pool() { ...@@ -165,7 +165,13 @@ export default function Pool() {
<ResponsiveButtonSecondary as={Link} padding="6px 8px" to="/create/ETH"> <ResponsiveButtonSecondary as={Link} padding="6px 8px" to="/create/ETH">
Create a pair Create a pair
</ResponsiveButtonSecondary> </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}> <Text fontWeight={500} fontSize={16}>
Add Liquidity Add Liquidity
</Text> </Text>
......
...@@ -3,6 +3,7 @@ import styled from 'styled-components' ...@@ -3,6 +3,7 @@ import styled from 'styled-components'
export const Wrapper = styled.div` export const Wrapper = styled.div`
position: relative; position: relative;
padding: 1rem;
` `
export const ClickableText = styled(Text)` export const ClickableText = styled(Text)`
......
...@@ -18,6 +18,8 @@ import { StyledInternalLink } from '../../theme' ...@@ -18,6 +18,8 @@ import { StyledInternalLink } from '../../theme'
import { currencyId } from '../../utils/currencyId' import { currencyId } from '../../utils/currencyId'
import AppBody from '../AppBody' import AppBody from '../AppBody'
import { Dots } from '../Pool/styleds' import { Dots } from '../Pool/styleds'
import { BlueCard } from '../../components/Card'
import { TYPE } from '../../theme'
enum Fields { enum Fields {
TOKEN0 = 0, TOKEN0 = 0,
...@@ -79,7 +81,14 @@ export default function PoolFinder() { ...@@ -79,7 +81,14 @@ export default function PoolFinder() {
return ( return (
<AppBody> <AppBody>
<FindPoolTabs /> <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&apos;t automatically appear in the interface.
</TYPE.link>
</AutoColumn>
</BlueCard>
<ButtonDropdownLight <ButtonDropdownLight
onClick={() => { onClick={() => {
setShowSearch(true) setShowSearch(true)
......
...@@ -9,7 +9,7 @@ import { RouteComponentProps } from 'react-router' ...@@ -9,7 +9,7 @@ import { RouteComponentProps } from 'react-router'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { ButtonPrimary, ButtonLight, ButtonError, ButtonConfirmed } from '../../components/Button' 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 { AutoColumn, ColumnCenter } from '../../components/Column'
import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal' import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyInputPanel from '../../components/CurrencyInputPanel'
...@@ -492,6 +492,14 @@ export default function RemoveLiquidity({ ...@@ -492,6 +492,14 @@ export default function RemoveLiquidity({
pendingText={pendingText} pendingText={pendingText}
/> />
<AutoColumn gap="md"> <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> <LightCard>
<AutoColumn gap="20px"> <AutoColumn gap="20px">
<RowBetween> <RowBetween>
......
...@@ -19,11 +19,12 @@ import { ArrowWrapper, BottomGrouping, SwapCallbackError, Wrapper } from '../../ ...@@ -19,11 +19,12 @@ import { ArrowWrapper, BottomGrouping, SwapCallbackError, Wrapper } from '../../
import TradePrice from '../../components/swap/TradePrice' import TradePrice from '../../components/swap/TradePrice'
import TokenWarningModal from '../../components/TokenWarningModal' import TokenWarningModal from '../../components/TokenWarningModal'
import ProgressSteps from '../../components/ProgressSteps' 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 { getTradeVersion } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useCurrency } from '../../hooks/Tokens' import { useCurrency, useDefaultTokens } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback' import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
import useENSAddress from '../../hooks/useENSAddress' import useENSAddress from '../../hooks/useENSAddress'
import { useSwapCallback } from '../../hooks/useSwapCallback' import { useSwapCallback } from '../../hooks/useSwapCallback'
...@@ -44,6 +45,8 @@ import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices' ...@@ -44,6 +45,8 @@ import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody' import AppBody from '../AppBody'
import { ClickableText } from '../Pool/styleds' import { ClickableText } from '../Pool/styleds'
import Loader from '../../components/Loader' import Loader from '../../components/Loader'
import { useIsTransactionUnsupported } from 'hooks/Trades'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import { isTradeBetter } from 'utils/trades' import { isTradeBetter } from 'utils/trades'
export default function Swap() { export default function Swap() {
...@@ -63,6 +66,14 @@ export default function Swap() { ...@@ -63,6 +66,14 @@ export default function Swap() {
setDismissTokenWarning(true) 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 { account } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
...@@ -101,12 +112,8 @@ export default function Swap() { ...@@ -101,12 +112,8 @@ export default function Swap() {
const trade = showWrap ? undefined : tradesByVersion[toggledVersion] const trade = showWrap ? undefined : tradesByVersion[toggledVersion]
const defaultTrade = showWrap ? undefined : tradesByVersion[DEFAULT_VERSION] const defaultTrade = showWrap ? undefined : tradesByVersion[DEFAULT_VERSION]
const betterTradeLinkVersion: Version | undefined = const betterTradeLinkV2: Version | undefined =
toggledVersion === Version.v2 && isTradeBetter(v2Trade, v1Trade, BETTER_TRADE_LINK_THRESHOLD) toggledVersion === Version.v1 && isTradeBetter(v1Trade, v2Trade) ? Version.v2 : undefined
? Version.v1
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, v2Trade)
? Version.v2
: undefined
const parsedAmounts = showWrap const parsedAmounts = showWrap
? { ? {
...@@ -282,15 +289,19 @@ export default function Swap() { ...@@ -282,15 +289,19 @@ export default function Swap() {
onCurrencySelection onCurrencySelection
]) ])
const swapIsUnsupported = useIsTransactionUnsupported(currencies?.INPUT, currencies?.OUTPUT)
return ( return (
<> <>
<TokenWarningModal <TokenWarningModal
isOpen={urlLoadedTokens.length > 0 && !dismissTokenWarning} isOpen={urlLoadedTokens.length > 0 && !dismissTokenWarning && importTokensNotInDefault}
tokens={urlLoadedTokens} tokens={urlLoadedTokens}
onConfirm={handleConfirmTokenWarning} onConfirm={handleConfirmTokenWarning}
/> />
<AppBody>
<SwapPoolTabs active={'swap'} /> <SwapPoolTabs active={'swap'} />
<AppBody>
<SwapHeader />
{/* <Separator /> */}
<Wrapper id="swap-page"> <Wrapper id="swap-page">
<ConfirmSwapModal <ConfirmSwapModal
isOpen={showConfirm} isOpen={showConfirm}
...@@ -363,8 +374,8 @@ export default function Swap() { ...@@ -363,8 +374,8 @@ export default function Swap() {
) : null} ) : null}
{showWrap ? null : ( {showWrap ? null : (
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}> <Card padding={showWrap ? '.25rem 1rem 0 1rem' : '0px'} borderRadius={'20px'}>
<AutoColumn gap="4px"> <AutoColumn gap="8px" style={{ padding: '0 16px' }}>
{Boolean(trade) && ( {Boolean(trade) && (
<RowBetween align="center"> <RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}> <Text fontWeight={500} fontSize={14} color={theme.text2}>
...@@ -392,7 +403,11 @@ export default function Swap() { ...@@ -392,7 +403,11 @@ export default function Swap() {
)} )}
</AutoColumn> </AutoColumn>
<BottomGrouping> <BottomGrouping>
{!account ? ( {swapIsUnsupported ? (
<ButtonPrimary disabled={true}>
<TYPE.main mb="4px">Unsupported Asset</TYPE.main>
</ButtonPrimary>
) : !account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight> <ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : showWrap ? ( ) : showWrap ? (
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap}> <ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap}>
...@@ -485,15 +500,19 @@ export default function Swap() { ...@@ -485,15 +500,19 @@ export default function Swap() {
</Column> </Column>
)} )}
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null} {isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
{betterTradeLinkVersion ? ( {betterTradeLinkV2 && !swapIsUnsupported && toggledVersion === Version.v1 ? (
<BetterTradeLink version={betterTradeLinkVersion} /> <BetterTradeLink version={betterTradeLinkV2} />
) : toggledVersion !== DEFAULT_VERSION && defaultTrade ? ( ) : toggledVersion !== DEFAULT_VERSION && defaultTrade ? (
<DefaultVersionLink /> <DefaultVersionLink />
) : null} ) : null}
</BottomGrouping> </BottomGrouping>
</Wrapper> </Wrapper>
</AppBody> </AppBody>
{!swapIsUnsupported ? (
<AdvancedSwapDetailsDropdown trade={trade} /> <AdvancedSwapDetailsDropdown trade={trade} />
) : (
<UnsupportedCurrencyFooter show={swapIsUnsupported} currencies={[currencies.INPUT, currencies.OUTPUT]} />
)}
</> </>
) )
} }
...@@ -10,9 +10,14 @@ export const fetchTokenList: Readonly<{ ...@@ -10,9 +10,14 @@ export const fetchTokenList: Readonly<{
fulfilled: createAction('lists/fetchTokenList/fulfilled'), fulfilled: createAction('lists/fetchTokenList/fulfilled'),
rejected: createAction('lists/fetchTokenList/rejected') rejected: createAction('lists/fetchTokenList/rejected')
} }
// add and remove from list options
export const acceptListUpdate = createAction<string>('lists/acceptListUpdate')
export const addList = createAction<string>('lists/addList') export const addList = createAction<string>('lists/addList')
export const removeList = createAction<string>('lists/removeList') 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') export const rejectVersionUpdate = createAction<Version>('lists/rejectVersionUpdate')
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 { ChainId, Token } from '@uniswap/sdk'
import { Tags, TokenInfo, TokenList } from '@uniswap/token-lists' import { Tags, TokenInfo, TokenList } from '@uniswap/token-lists'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { AppState } from '../index' 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] type TagDetails = Tags[keyof Tags]
export interface TagInfo extends TagDetails { export interface TagInfo extends TagDetails {
...@@ -25,7 +29,9 @@ export class WrappedTokenInfo extends Token { ...@@ -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. * An empty result, useful as a default.
...@@ -60,7 +66,10 @@ export function listToTokenMap(list: TokenList): TokenAddressMap { ...@@ -60,7 +66,10 @@ export function listToTokenMap(list: TokenList): TokenAddressMap {
...tokenMap, ...tokenMap,
[token.chainId]: { [token.chainId]: {
...tokenMap[token.chainId], ...tokenMap[token.chainId],
[token.address]: token [token.address]: {
token,
list: list
}
} }
} }
}, },
...@@ -70,49 +79,99 @@ export function listToTokenMap(list: TokenList): TokenAddressMap { ...@@ -70,49 +79,99 @@ export function listToTokenMap(list: TokenList): TokenAddressMap {
return map return map
} }
export function useTokenList(url: string | undefined): TokenAddressMap { export function useAllLists(): {
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl) 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)
}
function combineMaps(map1: TokenAddressMap, map2: TokenAddressMap): TokenAddressMap {
return {
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] }
}
}
// merge tokens contained within lists from urls
function useCombinedTokenMapFromUrls(urls: string[] | undefined): TokenAddressMap {
const lists = useAllLists()
return useMemo(() => { return useMemo(() => {
if (!url) return EMPTY_LIST if (!urls) return EMPTY_LIST
const current = lists[url]?.current
if (!current) 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 { try {
return listToTokenMap(current) const newTokens = Object.assign(listToTokenMap(current))
return combineMaps(allTokens, newTokens)
} catch (error) { } catch (error) {
console.error('Could not show token list due to error', error) console.error('Could not show token list due to error', error)
return EMPTY_LIST return allTokens
} }
}, [lists, url]) }, EMPTY_LIST)
)
}, [lists, urls])
} }
export function useSelectedListUrl(): string | undefined { // filter out unsupported lists
return useSelector<AppState, AppState['lists']['selectedListUrl']>(state => state.lists.selectedListUrl) export function useActiveListUrls(): string[] | undefined {
return useSelector<AppState, AppState['lists']['activeListUrls']>(state => state.lists.activeListUrls)?.filter(
url => !UNSUPPORTED_LIST_URLS.includes(url)
)
} }
export function useSelectedTokenList(): TokenAddressMap { export function useInactiveListUrls(): string[] {
return useTokenList(useSelectedListUrl()) const lists = useAllLists()
const allActiveListUrls = useActiveListUrls()
return Object.keys(lists).filter(url => !allActiveListUrls?.includes(url) && !UNSUPPORTED_LIST_URLS.includes(url))
} }
export function useSelectedListInfo(): { current: TokenList | null; pending: TokenList | null; loading: boolean } { // get all the tokens from active lists, combine with local default tokens
const selectedUrl = useSelectedListUrl() export function useCombinedActiveList(): TokenAddressMap {
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl) const activeListUrls = useActiveListUrls()
const list = selectedUrl ? listsByUrl[selectedUrl] : undefined const activeTokens = useCombinedTokenMapFromUrls(activeListUrls)
return { const defaultTokenMap = listToTokenMap(DEFAULT_TOKEN_LIST)
current: list?.current ?? null, return combineMaps(activeTokens, defaultTokenMap)
pending: list?.pendingUpdate ?? null,
loading: list?.loadingRequestId !== null
}
} }
// returns all downloaded current lists // all tokens from inactive lists
export function useAllLists(): TokenList[] { export function useCombinedInactiveList(): TokenAddressMap {
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl) const allInactiveListUrls: string[] = useInactiveListUrls()
return useCombinedTokenMapFromUrls(allInactiveListUrls)
}
return useMemo( // used to hide warnings on import for default tokens
() => export function useDefaultTokenList(): TokenAddressMap {
Object.keys(lists) return listToTokenMap(DEFAULT_TOKEN_LIST)
.map(url => lists[url].current) }
.filter((l): l is TokenList => Boolean(l)),
[lists] // 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))
} }
import { DEFAULT_ACTIVE_LIST_URLS } from './../../constants/lists'
import { createStore, Store } from 'redux' import { createStore, Store } from 'redux'
import { DEFAULT_LIST_OF_LISTS, DEFAULT_TOKEN_LIST_URL } from '../../constants/lists' import { DEFAULT_LIST_OF_LISTS, DEFAULT_TOKEN_LIST_URL } from '../../constants/lists'
import { updateVersion } from '../global/actions' 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' import reducer, { ListsState } from './reducer'
const STUB_TOKEN_LIST = { const STUB_TOKEN_LIST = {
...@@ -30,7 +31,7 @@ describe('list reducer', () => { ...@@ -30,7 +31,7 @@ describe('list reducer', () => {
beforeEach(() => { beforeEach(() => {
store = createStore(reducer, { store = createStore(reducer, {
byUrl: {}, byUrl: {},
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
...@@ -61,7 +62,7 @@ describe('list reducer', () => { ...@@ -61,7 +62,7 @@ describe('list reducer', () => {
loadingRequestId: null loadingRequestId: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
store.dispatch(fetchTokenList.pending({ requestId: 'request-id', url: 'fake-url' })) store.dispatch(fetchTokenList.pending({ requestId: 'request-id', url: 'fake-url' }))
...@@ -74,7 +75,7 @@ describe('list reducer', () => { ...@@ -74,7 +75,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
}) })
...@@ -93,7 +94,7 @@ describe('list reducer', () => { ...@@ -93,7 +94,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
...@@ -113,7 +114,7 @@ describe('list reducer', () => { ...@@ -113,7 +114,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
...@@ -134,7 +135,7 @@ describe('list reducer', () => { ...@@ -134,7 +135,7 @@ describe('list reducer', () => {
pendingUpdate: PATCHED_STUB_LIST pendingUpdate: PATCHED_STUB_LIST
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
it('does not save to current if list is newer minor version', () => { it('does not save to current if list is newer minor version', () => {
...@@ -154,7 +155,7 @@ describe('list reducer', () => { ...@@ -154,7 +155,7 @@ describe('list reducer', () => {
pendingUpdate: MINOR_UPDATED_STUB_LIST pendingUpdate: MINOR_UPDATED_STUB_LIST
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
it('does not save to pending if list is newer major version', () => { it('does not save to pending if list is newer major version', () => {
...@@ -174,7 +175,7 @@ describe('list reducer', () => { ...@@ -174,7 +175,7 @@ describe('list reducer', () => {
pendingUpdate: MAJOR_UPDATED_STUB_LIST pendingUpdate: MAJOR_UPDATED_STUB_LIST
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
}) })
...@@ -184,7 +185,7 @@ describe('list reducer', () => { ...@@ -184,7 +185,7 @@ describe('list reducer', () => {
store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' })) store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' }))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
byUrl: {}, byUrl: {},
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
...@@ -198,7 +199,7 @@ describe('list reducer', () => { ...@@ -198,7 +199,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' })) store.dispatch(fetchTokenList.rejected({ requestId: 'request-id', errorMessage: 'abcd', url: 'fake-url' }))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
...@@ -210,7 +211,7 @@ describe('list reducer', () => { ...@@ -210,7 +211,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
}) })
...@@ -228,7 +229,7 @@ describe('list reducer', () => { ...@@ -228,7 +229,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
it('no op for existing list', () => { it('no op for existing list', () => {
...@@ -241,7 +242,7 @@ describe('list reducer', () => { ...@@ -241,7 +242,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
store.dispatch(addList('fake-url')) store.dispatch(addList('fake-url'))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
...@@ -253,7 +254,7 @@ describe('list reducer', () => { ...@@ -253,7 +254,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
}) })
...@@ -269,7 +270,7 @@ describe('list reducer', () => { ...@@ -269,7 +270,7 @@ describe('list reducer', () => {
pendingUpdate: PATCHED_STUB_LIST pendingUpdate: PATCHED_STUB_LIST
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
store.dispatch(acceptListUpdate('fake-url')) store.dispatch(acceptListUpdate('fake-url'))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
...@@ -281,7 +282,7 @@ describe('list reducer', () => { ...@@ -281,7 +282,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
}) })
}) })
...@@ -297,15 +298,15 @@ describe('list reducer', () => { ...@@ -297,15 +298,15 @@ describe('list reducer', () => {
pendingUpdate: PATCHED_STUB_LIST pendingUpdate: PATCHED_STUB_LIST
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
store.dispatch(removeList('fake-url')) store.dispatch(removeList('fake-url'))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
byUrl: {}, 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, { store = createStore(reducer, {
byUrl: { byUrl: {
'fake-url': { 'fake-url': {
...@@ -315,18 +316,18 @@ describe('list reducer', () => { ...@@ -315,18 +316,18 @@ describe('list reducer', () => {
pendingUpdate: PATCHED_STUB_LIST pendingUpdate: PATCHED_STUB_LIST
} }
}, },
selectedListUrl: 'fake-url' activeListUrls: ['fake-url']
}) })
store.dispatch(removeList('fake-url')) store.dispatch(removeList('fake-url'))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
byUrl: {}, byUrl: {},
selectedListUrl: 'tokens.uniswap.eth' activeListUrls: []
}) })
}) })
}) })
describe('selectList', () => { describe('enableList', () => {
it('sets the selected list url', () => { it('enables a list url', () => {
store = createStore(reducer, { store = createStore(reducer, {
byUrl: { byUrl: {
'fake-url': { 'fake-url': {
...@@ -336,9 +337,9 @@ describe('list reducer', () => { ...@@ -336,9 +337,9 @@ describe('list reducer', () => {
pendingUpdate: PATCHED_STUB_LIST pendingUpdate: PATCHED_STUB_LIST
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
store.dispatch(selectList('fake-url')) store.dispatch(enableList('fake-url'))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
byUrl: { byUrl: {
'fake-url': { 'fake-url': {
...@@ -348,10 +349,10 @@ describe('list reducer', () => { ...@@ -348,10 +349,10 @@ describe('list reducer', () => {
pendingUpdate: PATCHED_STUB_LIST 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, { store = createStore(reducer, {
byUrl: { byUrl: {
'fake-url': { 'fake-url': {
...@@ -361,9 +362,9 @@ describe('list reducer', () => { ...@@ -361,9 +362,9 @@ describe('list reducer', () => {
pendingUpdate: PATCHED_STUB_LIST pendingUpdate: PATCHED_STUB_LIST
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
store.dispatch(selectList('fake-url-invalid')) store.dispatch(enableList('fake-url-invalid'))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
byUrl: { byUrl: {
'fake-url': { 'fake-url': {
...@@ -379,10 +380,10 @@ describe('list reducer', () => { ...@@ -379,10 +380,10 @@ describe('list reducer', () => {
pendingUpdate: null 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, { store = createStore(reducer, {
byUrl: { byUrl: {
'fake-url': { 'fake-url': {
...@@ -392,9 +393,9 @@ describe('list reducer', () => { ...@@ -392,9 +393,9 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
store.dispatch(selectList('fake-url')) store.dispatch(enableList('fake-url'))
expect(store.getState()).toEqual({ expect(store.getState()).toEqual({
byUrl: { byUrl: {
'fake-url': { 'fake-url': {
...@@ -404,7 +405,7 @@ describe('list reducer', () => { ...@@ -404,7 +405,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: 'fake-url' activeListUrls: ['fake-url']
}) })
}) })
}) })
...@@ -427,7 +428,7 @@ describe('list reducer', () => { ...@@ -427,7 +428,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined activeListUrls: undefined
}) })
store.dispatch(updateVersion()) store.dispatch(updateVersion())
}) })
...@@ -466,15 +467,7 @@ describe('list reducer', () => { ...@@ -466,15 +467,7 @@ describe('list reducer', () => {
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS) expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS)
}) })
it('sets selected list', () => { it('sets 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({
error: null,
current: null,
loadingRequestId: null,
pendingUpdate: null
})
}) })
}) })
describe('initialized with a different set of lists', () => { describe('initialized with a different set of lists', () => {
...@@ -494,7 +487,7 @@ describe('list reducer', () => { ...@@ -494,7 +487,7 @@ describe('list reducer', () => {
pendingUpdate: null pendingUpdate: null
} }
}, },
selectedListUrl: undefined, activeListUrls: undefined,
lastInitializedDefaultListOfLists: ['https://unpkg.com/@uniswap/default-token-list@latest'] lastInitializedDefaultListOfLists: ['https://unpkg.com/@uniswap/default-token-list@latest']
}) })
store.dispatch(updateVersion()) store.dispatch(updateVersion())
...@@ -538,7 +531,7 @@ describe('list reducer', () => { ...@@ -538,7 +531,7 @@ describe('list reducer', () => {
expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS) expect(store.getState().lastInitializedDefaultListOfLists).toEqual(DEFAULT_LIST_OF_LISTS)
}) })
it('sets default list to selected list', () => { 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', () => { it('default list is initialized', () => {
expect(store.getState().byUrl[DEFAULT_TOKEN_LIST_URL]).toEqual({ expect(store.getState().byUrl[DEFAULT_TOKEN_LIST_URL]).toEqual({
......
import { DEFAULT_ACTIVE_LIST_URLS } from './../../constants/lists'
import { createReducer } from '@reduxjs/toolkit' import { createReducer } from '@reduxjs/toolkit'
import { getVersionUpgrade, VersionUpgrade } from '@uniswap/token-lists' import { getVersionUpgrade, VersionUpgrade } from '@uniswap/token-lists'
import { TokenList } from '@uniswap/token-lists/dist/types' 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 { updateVersion } from '../global/actions'
import { acceptListUpdate, addList, fetchTokenList, removeList, selectList } from './actions' import { acceptListUpdate, addList, fetchTokenList, removeList, enableList, disableList } from './actions'
export interface ListsState { export interface ListsState {
readonly byUrl: { readonly byUrl: {
...@@ -16,7 +17,9 @@ export interface ListsState { ...@@ -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 // 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 lastInitializedDefaultListOfLists?: string[]
readonly selectedListUrl: string | undefined
// currently active lists
readonly activeListUrls: string[] | undefined
} }
type ListState = ListsState['byUrl'][string] type ListState = ListsState['byUrl'][string]
...@@ -38,7 +41,7 @@ const initialState: ListsState = { ...@@ -38,7 +41,7 @@ const initialState: ListsState = {
return memo return memo
}, {}) }, {})
}, },
selectedListUrl: DEFAULT_TOKEN_LIST_URL activeListUrls: DEFAULT_ACTIVE_LIST_URLS
} }
export default createReducer(initialState, builder => export default createReducer(initialState, builder =>
...@@ -59,6 +62,7 @@ export default createReducer(initialState, builder => ...@@ -59,6 +62,7 @@ export default createReducer(initialState, builder =>
// no-op if update does nothing // no-op if update does nothing
if (current) { if (current) {
const upgradeType = getVersionUpgrade(current.version, tokenList.version) const upgradeType = getVersionUpgrade(current.version, tokenList.version)
if (upgradeType === VersionUpgrade.NONE) return if (upgradeType === VersionUpgrade.NONE) return
if (loadingRequestId === null || loadingRequestId === requestId) { if (loadingRequestId === null || loadingRequestId === requestId) {
state.byUrl[url] = { state.byUrl[url] = {
...@@ -93,13 +97,6 @@ export default createReducer(initialState, builder => ...@@ -93,13 +97,6 @@ export default createReducer(initialState, builder =>
pendingUpdate: null pendingUpdate: null
} }
}) })
.addCase(selectList, (state, { payload: url }) => {
state.selectedListUrl = url
// automatically adds list
if (!state.byUrl[url]) {
state.byUrl[url] = NEW_LIST_STATE
}
})
.addCase(addList, (state, { payload: url }) => { .addCase(addList, (state, { payload: url }) => {
if (!state.byUrl[url]) { if (!state.byUrl[url]) {
state.byUrl[url] = NEW_LIST_STATE state.byUrl[url] = NEW_LIST_STATE
...@@ -109,8 +106,27 @@ export default createReducer(initialState, builder => ...@@ -109,8 +106,27 @@ export default createReducer(initialState, builder =>
if (state.byUrl[url]) { if (state.byUrl[url]) {
delete state.byUrl[url] delete state.byUrl[url]
} }
if (state.selectedListUrl === url) { // remove list from active urls if needed
state.selectedListUrl = url === DEFAULT_TOKEN_LIST_URL ? Object.keys(state.byUrl)[0] : DEFAULT_TOKEN_LIST_URL 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 }) => { .addCase(acceptListUpdate, (state, { payload: url }) => {
...@@ -127,7 +143,7 @@ export default createReducer(initialState, builder => ...@@ -127,7 +143,7 @@ export default createReducer(initialState, builder =>
// state loaded from localStorage, but new lists have never been initialized // state loaded from localStorage, but new lists have never been initialized
if (!state.lastInitializedDefaultListOfLists) { if (!state.lastInitializedDefaultListOfLists) {
state.byUrl = initialState.byUrl state.byUrl = initialState.byUrl
state.selectedListUrl = DEFAULT_TOKEN_LIST_URL state.activeListUrls = initialState.activeListUrls
} else if (state.lastInitializedDefaultListOfLists) { } else if (state.lastInitializedDefaultListOfLists) {
const lastInitializedSet = state.lastInitializedDefaultListOfLists.reduce<Set<string>>( const lastInitializedSet = state.lastInitializedDefaultListOfLists.reduce<Set<string>>(
(s, l) => s.add(l), (s, l) => s.add(l),
...@@ -150,11 +166,17 @@ export default createReducer(initialState, builder => ...@@ -150,11 +166,17 @@ export default createReducer(initialState, builder =>
state.lastInitializedDefaultListOfLists = DEFAULT_LIST_OF_LISTS state.lastInitializedDefaultListOfLists = DEFAULT_LIST_OF_LISTS
if (!state.selectedListUrl) { // if no active lists, activate defaults
state.selectedListUrl = DEFAULT_TOKEN_LIST_URL if (!state.activeListUrls) {
if (!state.byUrl[DEFAULT_TOKEN_LIST_URL]) { state.activeListUrls = DEFAULT_ACTIVE_LIST_URLS
state.byUrl[DEFAULT_TOKEN_LIST_URL] = NEW_LIST_STATE
// 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
})
} }
}) })
) )
import { useAllLists } from 'state/lists/hooks'
import { UNSUPPORTED_LIST_URLS } from './../../constants/lists'
import { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists' import { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists'
import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch } from 'react-redux'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useFetchListCallback } from '../../hooks/useFetchListCallback' import { useFetchListCallback } from '../../hooks/useFetchListCallback'
import useInterval from '../../hooks/useInterval' import useInterval from '../../hooks/useInterval'
import useIsWindowVisible from '../../hooks/useIsWindowVisible' import useIsWindowVisible from '../../hooks/useIsWindowVisible'
import { addPopup } from '../application/actions' import { AppDispatch } from '../index'
import { AppDispatch, AppState } from '../index'
import { acceptListUpdate } from './actions' import { acceptListUpdate } from './actions'
import { useActiveListUrls } from './hooks'
import { useAllInactiveTokens } from 'hooks/Tokens'
export default function Updater(): null { export default function Updater(): null {
const { library } = useActiveWeb3React() const { library } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>() 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 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(() => { const fetchAllListsCallback = useCallback(() => {
if (!isWindowVisible) return if (!isWindowVisible) return
Object.keys(lists).forEach(url => Object.keys(lists).forEach(url =>
...@@ -35,7 +39,6 @@ export default function Updater(): null { ...@@ -35,7 +39,6 @@ export default function Updater(): null {
useEffect(() => { useEffect(() => {
Object.keys(lists).forEach(listUrl => { Object.keys(lists).forEach(listUrl => {
const list = lists[listUrl] const list = lists[listUrl]
if (!list.current && !list.loadingRequestId && !list.error) { if (!list.current && !list.loadingRequestId && !list.error) {
fetchList(listUrl).catch(error => console.debug('list added fetching error', error)) fetchList(listUrl).catch(error => console.debug('list added fetching error', error))
} }
...@@ -57,21 +60,6 @@ export default function Updater(): null { ...@@ -57,21 +60,6 @@ export default function Updater(): null {
// automatically update minor/patch as long as bump matches the min update // automatically update minor/patch as long as bump matches the min update
if (bump >= min) { if (bump >= min) {
dispatch(acceptListUpdate(listUrl)) dispatch(acceptListUpdate(listUrl))
if (listUrl === selectedListUrl) {
dispatch(
addPopup({
key: listUrl,
content: {
listUpdate: {
listUrl,
oldList: list.current,
newList: list.pendingUpdate,
auto: true
}
}
})
)
}
} else { } else {
console.error( 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` `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 { ...@@ -80,26 +68,14 @@ export default function Updater(): null {
break break
case VersionUpgrade.MAJOR: case VersionUpgrade.MAJOR:
if (listUrl === selectedListUrl) { // accept update if list is active or list in background
dispatch( if (activeListUrls?.includes(listUrl) || UNSUPPORTED_LIST_URLS.includes(listUrl)) {
addPopup({ dispatch(acceptListUpdate(listUrl))
key: listUrl,
content: {
listUpdate: {
listUrl,
auto: false,
oldList: list.current,
newList: list.pendingUpdate
}
},
removeAfterMs: null
})
)
} }
} }
} }
}) })
}, [dispatch, lists, selectedListUrl]) }, [dispatch, lists, activeListUrls])
return null return null
} }
...@@ -17,6 +17,33 @@ export function useMintState(): AppState['mint'] { ...@@ -17,6 +17,33 @@ export function useMintState(): AppState['mint'] {
return useSelector<AppState, AppState['mint']>(state => state.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( export function useDerivedMintInfo(
currencyA: Currency | undefined, currencyA: Currency | undefined,
currencyB: Currency | undefined currencyB: Currency | undefined
...@@ -167,30 +194,3 @@ export function useDerivedMintInfo( ...@@ -167,30 +194,3 @@ export function useDerivedMintInfo(
error 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' ...@@ -3,7 +3,25 @@ import ReactGA from 'react-ga'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import styled, { keyframes } from 'styled-components' import styled, { keyframes } from 'styled-components'
import { darken } from 'polished' 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 }) => ({ export const Button = styled.button.attrs<{ warning: boolean }, { backgroundColor: string }>(({ warning, theme }) => ({
backgroundColor: warning ? theme.red1 : theme.primary1 backgroundColor: warning ? theme.red1 : theme.primary1
...@@ -39,6 +57,20 @@ export const CloseIcon = styled(X)<{ onClick: () => void }>` ...@@ -39,6 +57,20 @@ export const CloseIcon = styled(X)<{ onClick: () => void }>`
cursor: pointer; 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. // A button that triggers some onClick result, but looks like a link.
export const LinkStyledButton = styled.button<{ disabled?: boolean }>` export const LinkStyledButton = styled.button<{ disabled?: boolean }>`
border: none; border: none;
...@@ -104,6 +136,51 @@ const StyledLink = styled.a` ...@@ -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` const rotateImg = keyframes`
0% { 0% {
transform: perspective(1000px) rotateY(0deg); transform: perspective(1000px) rotateY(0deg);
...@@ -149,6 +226,36 @@ export function ExternalLink({ ...@@ -149,6 +226,36 @@ export function ExternalLink({
return <StyledLink target={target} rel={rel} href={href} onClick={handleClick} {...rest} /> 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` const rotate = keyframes`
from { from {
transform: rotate(0deg); transform: rotate(0deg);
......
...@@ -74,8 +74,9 @@ export function colors(darkMode: boolean): Colors { ...@@ -74,8 +74,9 @@ export function colors(darkMode: boolean): Colors {
secondary3: darkMode ? '#17000b26' : '#FDEAF1', secondary3: darkMode ? '#17000b26' : '#FDEAF1',
// other // other
red1: '#FF6871', red1: '#FD4040',
red2: '#F82D3A', red2: '#F82D3A',
red3: '#D60000',
green1: '#27AE60', green1: '#27AE60',
yellow1: '#FFE270', yellow1: '#FFE270',
yellow2: '#F3841E', yellow2: '#F3841E',
...@@ -156,7 +157,7 @@ export const TYPE = { ...@@ -156,7 +157,7 @@ export const TYPE = {
return <TextWrapper fontWeight={500} fontSize={11} {...props} /> return <TextWrapper fontWeight={500} fontSize={11} {...props} />
}, },
blue(props: TextProps) { blue(props: TextProps) {
return <TextWrapper fontWeight={500} color={'primary1'} {...props} /> return <TextWrapper fontWeight={500} color={'blue1'} {...props} />
}, },
yellow(props: TextProps) { yellow(props: TextProps) {
return <TextWrapper fontWeight={500} color={'yellow1'} {...props} /> return <TextWrapper fontWeight={500} color={'yellow1'} {...props} />
......
...@@ -40,6 +40,7 @@ export interface Colors { ...@@ -40,6 +40,7 @@ export interface Colors {
// other // other
red1: Color red1: Color
red2: Color red2: Color
red3: Color
green1: Color green1: Color
yellow1: Color yellow1: Color
yellow2: Color yellow2: Color
......
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) ...@@ -10,8 +10,8 @@ const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(BASE_FEE)
// computes price breakdown for the trade // computes price breakdown for the trade
export function computeTradePriceBreakdown( export function computeTradePriceBreakdown(
trade?: Trade trade?: Trade | null
): { priceImpactWithoutFee?: Percent; realizedLPFee?: CurrencyAmount } { ): { 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 // 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)) // e.g. for 3 tokens/2 hops: 1 - ((1 - .03) * (1-.03))
const realizedLPFee = !trade const realizedLPFee = !trade
......
import { Trade, currencyEquals, Percent } from '@uniswap/sdk' import { ZERO_PERCENT, ONE_HUNDRED_PERCENT } from './../constants/index'
import { Trade, Percent, currencyEquals } from '@uniswap/sdk'
const ZERO_PERCENT = new Percent('0')
const ONE_HUNDRED_PERCENT = new Percent('1')
// returns whether tradeB is better than tradeA by at least a threshold percentage amount // returns whether tradeB is better than tradeA by at least a threshold percentage amount
export function isTradeBetter( export function isTradeBetter(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment