Commit c3d8bc7e authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

Localstorage and routing improvements (#704)

* remove tokens context in favor of localstorage

refactor localstorage

improve adding custom token flow

* drop exchange language

* ensure url tokens are added to localstorage

clean up routing

remove unnecessary output approval checks

* fix bad import

* Remove unused imported checks

* remove unused imports
Co-authored-by: default avatarIan Lapham <ianlapham@gmail.com>
parent 3f1d7ab3
{
"name": "uniswap",
"description": "Uniswap Exchange Protocol",
"description": "Uniswap Protocol",
"version": "0.1.0",
"homepage": "https://uniswap.exchange",
"private": true,
......
......@@ -20,7 +20,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Uniswap Exchange</title>
<title>Uniswap</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
......
{
"short_name": "Uniswap",
"name": "Uniswap Exchange",
"name": "Uniswap",
"icons": [
{
"src": "favicon.ico",
......
......@@ -86,7 +86,7 @@ export default function AdvancedSettings({ setIsOpen, setDeadline, allowedSlippa
back
</Link>
<RowBetween>
<TYPE.main>Limit front-running tolerance</TYPE.main>
<TYPE.main>Front-running tolerance</TYPE.main>
<QuestionHelper text={t('toleranceExplanation')} />
</RowBetween>
<Row>
......
......@@ -25,7 +25,7 @@ import { ButtonPrimary, ButtonError, ButtonLight } from '../Button'
import { GreyCard, BlueCard, YellowCard, LightCard } from '../../components/Card'
import { usePair } from '../../contexts/Pairs'
import { useToken } from '../../contexts/Tokens'
import { useToken, useAllTokens } from '../../contexts/Tokens'
import { useRoute } from '../../contexts/Routes'
import { useAddressAllowance } from '../../contexts/Allowances'
import { useWeb3React, useTokenContract } from '../../hooks'
......@@ -33,8 +33,8 @@ import { useAddressBalance, useAllBalances } from '../../contexts/Balances'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
import { ROUTER_ADDRESSES } from '../../constants'
// import { INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
import { getRouterContract, calculateGasMargin, getProviderOrSigner, getEtherscanLink } from '../../utils'
import { getRouterContract, calculateGasMargin, getProviderOrSigner, getEtherscanLink, isWETH } from '../../utils'
import { useLocalStorageTokens } from '../../contexts/LocalStorage'
const Wrapper = styled.div`
position: relative;
......@@ -286,7 +286,7 @@ const DEFAULT_DEADLINE_FROM_NOW = 60 * 20
const ALLOWED_SLIPPAGE_MEDIUM = 100
const ALLOWED_SLIPPAGE_HIGH = 500
function ExchangePage({ sendingInput = false, history, initialCurrency, params }) {
function ExchangePage({ sendingInput = false, history, params }) {
// text translation
// const { t } = useTranslation()
......@@ -307,11 +307,7 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params }
reducer,
{
independentField: params.outputTokenAddress && !params.inputTokenAddress ? Field.OUTPUT : Field.INPUT,
inputTokenAddress: params.inputTokenAddress
? params.inputTokenAddress
: initialCurrency
? initialCurrency
: WETH[chainId].address,
inputTokenAddress: params.inputTokenAddress ? params.inputTokenAddress : WETH[chainId].address,
outputTokenAddress: params.outputTokenAddress ? params.outputTokenAddress : '',
typedValue:
params.inputTokenAddress && !params.outputTokenAddress
......@@ -340,6 +336,30 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params }
[Field.OUTPUT]: useToken(fieldData[Field.OUTPUT].address)
}
// ensure input + output tokens are added to localstorage
const [, { fetchTokenByAddress, addToken }] = useLocalStorageTokens()
const allTokens = useAllTokens()
const inputTokenAddress = fieldData[Field.INPUT].address
useEffect(() => {
if (inputTokenAddress && !Object.keys(allTokens).some(tokenAddress => tokenAddress === inputTokenAddress)) {
fetchTokenByAddress(inputTokenAddress).then(token => {
if (token !== null) {
addToken(token)
}
})
}
}, [inputTokenAddress, allTokens, fetchTokenByAddress, addToken])
const outputTokenAddress = fieldData[Field.OUTPUT].address
useEffect(() => {
if (outputTokenAddress && !Object.keys(allTokens).some(tokenAddress => tokenAddress === outputTokenAddress)) {
fetchTokenByAddress(outputTokenAddress).then(token => {
if (token !== null) {
addToken(token)
}
})
}
}, [outputTokenAddress, allTokens, fetchTokenByAddress, addToken])
// token contracts for approvals and direct sends
const tokenContractInput: ethers.Contract = useTokenContract(tokens[Field.INPUT]?.address)
const tokenContractOutput: ethers.Contract = useTokenContract(tokens[Field.OUTPUT]?.address)
......@@ -347,11 +367,6 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params }
// check on pending approvals for token amounts
const pendingApprovalInput = usePendingApproval(tokens[Field.INPUT]?.address)
// check for imported tokens to show warning
// const importedTokenInput = tokens[Field.INPUT] && !!!INITIAL_TOKENS_CONTEXT?.[chainId]?.[tokens[Field.INPUT]?.address]
// const importedTokenOutput =
// tokens[Field.OUTPUT] && !!!INITIAL_TOKENS_CONTEXT?.[chainId]?.[tokens[Field.OUTPUT]?.address]
// entities for swap
const pair: Pair = usePair(tokens[Field.INPUT], tokens[Field.OUTPUT])
const route = useRoute(tokens[Field.INPUT], tokens[Field.OUTPUT])
......@@ -371,7 +386,7 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params }
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
// approvals
// input approval
const inputApproval: TokenAmount = useAddressAllowance(account, tokens[Field.INPUT], routerAddress)
// all balances for detecting a swap with send
......@@ -477,11 +492,8 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params }
!!userBalances[Field.INPUT] &&
!!tokens[Field.INPUT] &&
WETH[chainId] &&
JSBI.greaterThan(
userBalances[Field.INPUT].raw,
tokens[Field.INPUT].equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
)
? tokens[Field.INPUT].equals(WETH[chainId])
JSBI.greaterThan(userBalances[Field.INPUT].raw, isWETH(tokens[Field.INPUT]) ? MIN_ETHER.raw : JSBI.BigInt(0))
? isWETH(tokens[Field.INPUT])
? userBalances[Field.INPUT].subtract(MIN_ETHER)
: userBalances[Field.INPUT]
: undefined
......
......@@ -3,8 +3,6 @@ import '@reach/tooltip/styles.css'
import styled from 'styled-components'
import escapeStringRegex from 'escape-string-regexp'
import { JSBI, WETH } from '@uniswap/sdk'
import { Link } from 'react-router-dom'
import { ethers } from 'ethers'
import { isMobile } from 'react-device-detect'
import { withRouter } from 'react-router-dom'
import { COMMON_BASES } from '../../constants'
......@@ -17,21 +15,19 @@ import DoubleTokenLogo from '../DoubleLogo'
import Column, { AutoColumn } from '../Column'
import { Text } from 'rebass'
import { Hover } from '../../theme'
import { LightCard } from '../Card'
import { ArrowLeft } from 'react-feather'
import { ArrowLeft, X } from 'react-feather'
import { CloseIcon } from '../../theme/components'
import { ColumnCenter } from '../../components/Column'
import { Spinner, TYPE } from '../../theme'
import { ButtonSecondary } from '../Button'
import { RowBetween, RowFixed, AutoRow } from '../Row'
import { isAddress } from '../../utils'
import { useAllPairs } from '../../contexts/Pairs'
import { useWeb3React } from '../../hooks'
import { useSavedTokens } from '../../contexts/LocalStorage'
import { useLocalStorageTokens } from '../../contexts/LocalStorage'
import { useAllBalances } from '../../contexts/Balances'
import { useTranslation } from 'react-i18next'
import { useToken, useAllTokens, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
import { useToken, useAllTokens, ALL_TOKENS } from '../../contexts/Tokens'
import QuestionHelper from '../Question'
const TokenModalInfo = styled.div`
......@@ -52,6 +48,8 @@ const TokenList = styled.div`
const FadedSpan = styled.span`
color: ${({ theme }) => theme.blue1};
display: flex;
align-items: center;
`
const GreySpan = styled.span`
......@@ -112,6 +110,7 @@ const PaddedItem = styled(RowBetween)`
const MenuItem = styled(PaddedItem)`
cursor: ${({ disabled }) => !disabled && 'pointer'};
pointer-events: ${({ disabled }) => disabled && 'none'};
:hover {
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
}
......@@ -158,25 +157,36 @@ function SearchModal({
const allTokens = useAllTokens()
const allPairs = useAllPairs()
const allBalances = useAllBalances()
const [searchQuery, setSearchQuery] = useState('')
const [sortDirection, setSortDirection] = useState(true)
const token = useToken(searchQuery)
const tokenAddress = token && token.address
const [, { fetchTokenByAddress, addToken, removeTokenByAddress }] = useLocalStorageTokens()
// amount of tokens to display at once
const [, setTokensShown] = useState(0)
const [, setPairsShown] = useState(0)
// if the current input is an address, and we don't have the token in context, try to fetch it
const token = useToken(searchQuery)
const [temporaryToken, setTemporaryToken] = useState()
useEffect(() => {
const address = isAddress(searchQuery)
if (address && !token) {
let stale = false
fetchTokenByAddress(address).then(token => {
if (!stale) {
setTemporaryToken(token)
}
})
return () => {
stale = true
setTemporaryToken()
}
}
}, [searchQuery, token, fetchTokenByAddress])
const [activeFilter, setActiveFilter] = useState(FILTERS.BALANCES)
const [showTokenImport, setShowTokenImport] = useState(false)
const [, saveUserToken] = useSavedTokens()
// reset view on close
useEffect(() => {
if (!isOpen) {
......@@ -227,8 +237,8 @@ function SearchModal({
const urlAdded = urlAddedTokens && urlAddedTokens.hasOwnProperty(tokenEntry.address)
const customAdded =
tokenEntry.address !== 'ETH' &&
INITIAL_TOKENS_CONTEXT[chainId] &&
!INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(tokenEntry.address) &&
ALL_TOKENS[chainId] &&
!ALL_TOKENS[chainId].hasOwnProperty(tokenEntry.address) &&
!urlAdded
// if token import page dont show preset list, else show all
......@@ -324,14 +334,6 @@ function SearchModal({
})
}, [allPairs, allTokens, searchQuery, sortedPairList])
// update the amount shown as filtered list changes
useEffect(() => {
setTokensShown(Math.min(Object.keys(filteredTokenList).length, 3))
}, [filteredTokenList])
useEffect(() => {
setPairsShown(Math.min(Object.keys(filteredPairList).length, 3))
}, [filteredPairList])
function renderPairsList() {
if (filteredPairList?.length === 0) {
return (
......@@ -369,93 +371,127 @@ function SearchModal({
}
function renderTokenList() {
if (isAddress(searchQuery) && tokenAddress === undefined) {
return <Text>Searching for Exchange...</Text>
}
if (isAddress(searchQuery) && tokenAddress === ethers.constants.AddressZero) {
return (
<>
<TokenModalInfo>{t('noToken')}</TokenModalInfo>
<TokenModalInfo>
<Link to={`/create-exchange/${searchQuery}`}>{t('createExchange')}</Link>
</TokenModalInfo>
</>
)
if (filteredTokenList.length === 0) {
if (isAddress(searchQuery)) {
if (temporaryToken === undefined) {
return <TokenModalInfo>Searching for Token...</TokenModalInfo>
} else if (temporaryToken === null) {
return <TokenModalInfo>Address is not a valid ERC-20 token.</TokenModalInfo>
} else {
// a user found a token by search that isn't yet added to localstorage
return (
<MenuItem
key={temporaryToken.address}
onClick={() => {
addToken(temporaryToken)
_onTokenSelect(temporaryToken.address)
}}
>
<RowFixed>
<TokenLogo address={temporaryToken.address} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>{temporaryToken.symbol}</Text>
<FadedSpan>(Found by search)</FadedSpan>
</Column>
</RowFixed>
</MenuItem>
)
}
} else {
return <TokenModalInfo>{t('noToken')}</TokenModalInfo>
}
}
if (!filteredTokenList.length) {
return <TokenModalInfo>{t('noToken')}</TokenModalInfo>
// TODO is this the right place to link to create exchange?
// else if (isAddress(searchQuery) && tokenAddress === ethers.constants.AddressZero) {
// return (
// <>
// <TokenModalInfo>{t('noToken')}</TokenModalInfo>
// <TokenModalInfo>
// <Link to={`/create-exchange/${searchQuery}`}>{t('createExchange')}</Link>
// </TokenModalInfo>
// </>
// )
// }
else {
return filteredTokenList
.sort((a, b) => {
if (b?.address === WETH[chainId]?.address) {
return 1
} else
return parseFloat(a?.balance?.toExact()) > parseFloat(b?.balance?.toExact())
? sortDirection
? -1
: 1
: sortDirection
? 1
: -1
})
.map(({ address, symbol, balance }) => {
const urlAdded = urlAddedTokens && urlAddedTokens.hasOwnProperty(address)
const customAdded =
address !== 'ETH' && ALL_TOKENS[chainId] && !ALL_TOKENS[chainId].hasOwnProperty(address) && !urlAdded
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
// if token import page dont show preset list, else show all
return (
<MenuItem
key={address}
onClick={() => (hiddenToken && hiddenToken === address ? () => {} : _onTokenSelect(address))}
disabled={hiddenToken && hiddenToken === address}
>
<RowFixed>
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>
{symbol}
{otherSelectedTokenAddress === address && <GreySpan> ({otherSelectedText})</GreySpan>}
</Text>
<FadedSpan>
{urlAdded && '(Added by URL)'} {customAdded && '(Added by user)'}
{customAdded && (
<X
style={{ transform: 'scale(0.8)' }}
onClick={event => {
event.stopPropagation()
if (searchQuery === address) {
setSearchQuery('')
}
removeTokenByAddress(chainId, address)
}}
/>
)}
</FadedSpan>
</Column>
</RowFixed>
<AutoColumn gap="4px" justify="end">
{balance ? (
<Text>
{zeroBalance && showSendWithSwap ? (
<ColumnCenter
justify="center"
style={{ backgroundColor: '#EBF4FF', padding: '8px', borderRadius: '12px' }}
>
<Text textAlign="center" fontWeight={500} color="#2172E5">
Send With Swap
</Text>
</ColumnCenter>
) : balance ? (
balance.toSignificant(6)
) : (
'-'
)}
</Text>
) : account ? (
<SpinnerWrapper src={Circle} alt="loader" />
) : (
'-'
)}
</AutoColumn>
</MenuItem>
)
})
}
return filteredTokenList
.sort((a, b) => {
if (b?.address === WETH[chainId]?.address) {
return 1
} else
return parseFloat(a?.balance?.toExact()) > parseFloat(b?.balance?.toExact())
? sortDirection
? -1
: 1
: sortDirection
? 1
: -1
})
.map(({ address, symbol, balance }) => {
const urlAdded = urlAddedTokens && urlAddedTokens.hasOwnProperty(address)
const customAdded =
address !== 'ETH' &&
INITIAL_TOKENS_CONTEXT[chainId] &&
!INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(address) &&
!urlAdded
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
// if token import page dont show preset list, else show all
return (
<MenuItem
key={address}
onClick={() => (hiddenToken && hiddenToken === address ? () => {} : _onTokenSelect(address))}
disabled={hiddenToken && hiddenToken === address}
>
<RowFixed>
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>
{symbol}
{otherSelectedTokenAddress === address && <GreySpan> ({otherSelectedText})</GreySpan>}
</Text>
<FadedSpan>
{urlAdded && '(Added by URL)'} {customAdded && '(Added by user)'}
</FadedSpan>
</Column>
</RowFixed>
<AutoColumn gap="4px" justify="end">
{balance ? (
<Text>
{zeroBalance && showSendWithSwap ? (
<ColumnCenter
justify="center"
style={{ backgroundColor: '#EBF4FF', padding: '8px', borderRadius: '12px' }}
>
<Text textAlign="center" fontWeight={500} color="#2172E5">
Send With Swap
</Text>
</ColumnCenter>
) : balance ? (
balance.toSignificant(6)
) : (
'-'
)}
</Text>
) : account ? (
<SpinnerWrapper src={Circle} alt="loader" />
) : (
'-'
)}
</AutoColumn>
</MenuItem>
)
})
}
const Filter = ({ title, filter }) => {
......@@ -515,31 +551,7 @@ function SearchModal({
ref={inputRef}
onChange={onInput}
/>
<LightCard padding={filteredTokenList?.length === 0 ? '20px' : '0px'}>
{filteredTokenList?.length === 0 ? (
<AutoColumn gap="8px" justify="center">
<TYPE.body color="">No token found.</TYPE.body>
</AutoColumn>
) : (
renderTokenList()
)}
</LightCard>
{filteredTokenList?.length > 0 && (
<RowBetween>
<ButtonSecondary
width="48%"
onClick={() => {
const newToken = filteredTokenList?.[0]
saveUserToken(newToken?.address)
}}
>
Save To Your List
</ButtonSecondary>
<ButtonSecondary width="48%" onClick={() => _onTokenSelect(filteredTokenList[0].address)}>
Import
</ButtonSecondary>
</RowBetween>
)}
{renderTokenList()}
</PaddedColumn>
) : (
<PaddedColumn gap="20px">
......
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
import { useAllTokens } from './Tokens'
const UNISWAP = 'UNISWAP'
const VERSION = 'VERSION'
const CURRENT_VERSION = 0
const LAST_SAVED = 'LAST_SAVED'
const BETA_MESSAGE_DISMISSED = 'BETA_MESSAGE_DISMISSED'
const MIGRATION_MESSAGE_DISMISSED = 'MIGRATION_MESSAGE_DISMISSED'
const DARK_MODE = 'DARK_MODE'
const TOKEN_LIST = 'TOKEN_LIST'
const UPDATABLE_KEYS = [BETA_MESSAGE_DISMISSED, MIGRATION_MESSAGE_DISMISSED, DARK_MODE]
const UPDATE_KEY = 'UPDATE_KEY'
const UPDATE_TOKEN_LIST = 'UPDATE_TOKEN_LIST'
const LocalStorageContext = createContext()
function useLocalStorageContext() {
return useContext(LocalStorageContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case UPDATE_KEY: {
const { key, value } = payload
if (!UPDATABLE_KEYS.some(k => k === key)) {
throw Error(`Unexpected key in LocalStorageContext reducer: '${key}'.`)
} else {
return {
...state,
[key]: value
}
}
}
case UPDATE_TOKEN_LIST: {
const { tokenAddress, token } = payload
return {
...state,
[TOKEN_LIST]: {
...state?.[TOKEN_LIST],
[tokenAddress]: token
}
}
}
default: {
throw Error(`Unexpected action type in LocalStorageContext reducer: '${type}'.`)
}
}
}
function init() {
const defaultLocalStorage = {
[VERSION]: CURRENT_VERSION,
[BETA_MESSAGE_DISMISSED]: false,
[MIGRATION_MESSAGE_DISMISSED]: false,
[DARK_MODE]: false
}
try {
const parsed = JSON.parse(window.localStorage.getItem(UNISWAP))
if (parsed[VERSION] !== CURRENT_VERSION) {
// this is where we could run migration logic
return defaultLocalStorage
} else {
return { ...defaultLocalStorage, ...parsed }
}
} catch {
return defaultLocalStorage
}
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, undefined, init)
const updateKey = useCallback((key, value) => {
dispatch({ type: UPDATE_KEY, payload: { key, value } })
}, [])
const updateTokenList = useCallback((tokenAddress, token) => {
dispatch({ type: UPDATE_TOKEN_LIST, payload: { tokenAddress, token } })
}, [])
return (
<LocalStorageContext.Provider
value={useMemo(() => [state, { updateKey, updateTokenList }], [state, updateKey, updateTokenList])}
>
{children}
</LocalStorageContext.Provider>
)
}
export function Updater() {
const [state] = useLocalStorageContext()
useEffect(() => {
window.localStorage.setItem(UNISWAP, JSON.stringify({ ...state, [LAST_SAVED]: Math.floor(Date.now() / 1000) }))
})
return null
}
export function useBetaMessageManager() {
const [state, { updateKey }] = useLocalStorageContext()
const dismissBetaMessage = useCallback(() => {
updateKey(BETA_MESSAGE_DISMISSED, true)
}, [updateKey])
return [!state[BETA_MESSAGE_DISMISSED], dismissBetaMessage]
}
export function useMigrationMessageManager() {
const [state, { updateKey }] = useLocalStorageContext()
const dismissMigrationMessage = useCallback(() => {
updateKey(MIGRATION_MESSAGE_DISMISSED, true)
}, [updateKey])
return [!state[MIGRATION_MESSAGE_DISMISSED], dismissMigrationMessage]
}
export function useDarkModeManager() {
const [state, { updateKey }] = useLocalStorageContext()
let isDarkMode = state[DARK_MODE]
const toggleDarkMode = useCallback(
value => {
updateKey(DARK_MODE, value === false || value === true ? value : !isDarkMode)
},
[updateKey, isDarkMode]
)
return [state[DARK_MODE], toggleDarkMode]
}
/**
* @todo is there a better place to store these? should we move into tokens context
*/
export function useSavedTokens() {
const [state, { updateTokenList }] = useLocalStorageContext()
const allTokens = useAllTokens()
const userList = state?.[TOKEN_LIST] || []
function addToken(tokenAddress) {
const token = allTokens?.[tokenAddress]
if (token) {
updateTokenList(token.address, token)
}
}
return [userList, addToken]
}
import React, { createContext, useContext, useMemo, useCallback, useEffect, useState } from 'react'
import { Token } from '@uniswap/sdk'
import { getTokenDecimals, getTokenSymbol, getTokenName, isAddress } from '../utils'
import { useWeb3React } from '@web3-react/core'
enum LocalStorageKeys {
VERSION = 'version',
LAST_SAVED = 'lastSaved',
BETA_MESSAGE_DISMISSED = 'betaMessageDismissed',
MIGRATION_MESSAGE_DISMISSED = 'migrationMessageDismissed',
DARK_MODE = 'darkMode',
TOKENS = 'tokens'
}
function useLocalStorage<T, S = T>(
key: LocalStorageKeys,
defaultValue: T,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{ serialize, deserialize }: { serialize: (toSerialize: T) => S; deserialize: (toDeserialize: S) => T } = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
serialize: (toSerialize): S => (toSerialize as unknown) as S,
deserialize: (toDeserialize): T => (toDeserialize as unknown) as T
}
): [T, (value: T) => void] {
const [value, setValue] = useState(() => {
try {
return deserialize(JSON.parse(window.localStorage.getItem(key))) ?? defaultValue
} catch {
return defaultValue
}
})
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(serialize(value)))
} catch {}
}, [key, serialize, value])
return [value, setValue]
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function serializeTokens(
tokens: Token[]
): { chainId: number; address: string; decimals: number; symbol: string; name: string }[] {
return tokens.map(token => ({
chainId: token.chainId,
address: token.address,
decimals: token.decimals,
symbol: token.symbol,
name: token.name
}))
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function deserializeTokens(serializedTokens: ReturnType<typeof serializeTokens>): Token[] {
return serializedTokens.map(
serializedToken =>
new Token(
serializedToken.chainId,
serializedToken.address,
serializedToken.decimals,
serializedToken.symbol,
serializedToken.name
)
)
}
const LocalStorageContext = createContext<[any, any]>([{}, {}])
function useLocalStorageContext() {
return useContext(LocalStorageContext)
}
export default function Provider({ children }) {
// global localstorage state
const [version, setVersion] = useLocalStorage<number>(LocalStorageKeys.VERSION, 0)
const [lastSaved, setLastSaved] = useLocalStorage<number>(LocalStorageKeys.LAST_SAVED, Math.floor(Date.now() / 1000))
const [betaMessageDismissed, setBetaMessageDismissed] = useLocalStorage<boolean>(
LocalStorageKeys.BETA_MESSAGE_DISMISSED,
false
)
const [migrationMessageDismissed, setMigrationMessageDismissed] = useLocalStorage<boolean>(
LocalStorageKeys.MIGRATION_MESSAGE_DISMISSED,
false
)
const [darkMode, setDarkMode] = useLocalStorage<boolean>(LocalStorageKeys.DARK_MODE, false)
const [tokens, setTokens] = useLocalStorage<Token[], ReturnType<typeof serializeTokens>>(
LocalStorageKeys.TOKENS,
[],
{
serialize: serializeTokens,
deserialize: deserializeTokens
}
)
return (
<LocalStorageContext.Provider
value={useMemo(
() => [
{ version, lastSaved, betaMessageDismissed, migrationMessageDismissed, darkMode, tokens },
{
setVersion,
setLastSaved,
setBetaMessageDismissed,
setMigrationMessageDismissed,
setDarkMode,
setTokens
}
],
[
version,
lastSaved,
betaMessageDismissed,
migrationMessageDismissed,
darkMode,
tokens,
setVersion,
setLastSaved,
setBetaMessageDismissed,
setMigrationMessageDismissed,
setDarkMode,
setTokens
]
)}
>
{children}
</LocalStorageContext.Provider>
)
}
export function useBetaMessageManager() {
const [{ betaMessageDismissed }, { setBetaMessageDismissed }] = useLocalStorageContext()
const dismissBetaMessage = useCallback(() => {
setBetaMessageDismissed(true)
}, [setBetaMessageDismissed])
return [!betaMessageDismissed, dismissBetaMessage]
}
export function useMigrationMessageManager() {
const [{ migrationMessageDismissed }, { setMigrationMessageDismissed }] = useLocalStorageContext()
const dismissMigrationMessage = useCallback(() => {
setMigrationMessageDismissed(true)
}, [setMigrationMessageDismissed])
return [!migrationMessageDismissed, dismissMigrationMessage]
}
export function useDarkModeManager() {
const [{ darkMode }, { setDarkMode }] = useLocalStorageContext()
const toggleSetDarkMode = useCallback(
value => {
setDarkMode(typeof value === 'boolean' ? value : !darkMode)
},
[darkMode, setDarkMode]
)
return [darkMode, toggleSetDarkMode]
}
export function useLocalStorageTokens(): [
Token[],
{
fetchTokenByAddress: (address: string) => Promise<Token | null>
addToken: (token: Token) => void
removeTokenByAddress: (chainId: number, address: string) => void
}
] {
const { library, chainId } = useWeb3React()
const [{ tokens }, { setTokens }] = useLocalStorageContext()
const fetchTokenByAddress = useCallback(
async (address: string) => {
const [decimals, symbol, name] = await Promise.all([
getTokenDecimals(address, library).catch(() => null),
getTokenSymbol(address, library).catch(() => 'UNKNOWN'),
getTokenName(address, library).catch(() => 'Unknown')
])
if (decimals === null) {
return null
} else {
return new Token(chainId, address, decimals, symbol, name)
}
},
[library, chainId]
)
const addToken = useCallback(
(token: Token) => {
setTokens(tokens => tokens.filter(currentToken => !currentToken.equals(token)).concat([token]))
},
[setTokens]
)
const removeTokenByAddress = useCallback(
(chainId: number, address: string) => {
setTokens(tokens =>
tokens.filter(
currentToken => !(currentToken.chainId === chainId && currentToken.address === isAddress(address))
)
)
},
[setTokens]
)
return [tokens, { fetchTokenByAddress, addToken, removeTokenByAddress }]
}
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect, useState } from 'react'
import { useAddressBalance } from './Balances'
import { useWeb3React, usePairContract } from '../hooks'
import { INITIAL_TOKENS_CONTEXT } from './Tokens'
import { ALL_TOKENS } from './Tokens'
import { ChainId, WETH, Token, TokenAmount, Pair, JSBI } from '@uniswap/sdk'
const ADDRESSES_KEY = 'ADDRESSES_KEY'
......@@ -12,12 +12,12 @@ const UPDATE_PAIR_ENTITY = 'UPDATE_PAIR_ENTITY'
const ALL_PAIRS: [Token, Token][] = [
[
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY][WETH[ChainId.RINKEBY].address],
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'] //dai
ALL_TOKENS[ChainId.RINKEBY][WETH[ChainId.RINKEBY].address],
ALL_TOKENS[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'] //dai
],
[
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'], // dai
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44'] // mkr
ALL_TOKENS[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'], // dai
ALL_TOKENS[ChainId.RINKEBY]['0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44'] // mkr
]
]
......
......@@ -2,6 +2,7 @@ import React, { createContext, useContext, useReducer, useMemo, useCallback, use
import { WETH, Token, Route, JSBI } from '@uniswap/sdk'
import { useWeb3React } from '../hooks'
import { usePair } from '../contexts/Pairs'
import { isWETH } from '../utils'
const UPDATE = 'UPDATE'
......@@ -77,8 +78,8 @@ export function useRoute(tokenA: Token, tokenB: Token) {
const defaultPair = usePair(tokenA, tokenB)
// get token<->WETH pairs
const aToETH = usePair(tokenA && !tokenA.equals(WETH[chainId]) ? tokenA : null, WETH[chainId])
const bToETH = usePair(tokenB && !tokenB.equals(WETH[chainId]) ? tokenB : null, WETH[chainId])
const aToETH = usePair(tokenA && !isWETH(tokenA) ? tokenA : null, WETH[chainId])
const bToETH = usePair(tokenB && !isWETH(tokenB) ? tokenB : null, WETH[chainId])
// needs to route through WETH
const requiresHop =
......
import { useMemo } from 'react'
import { ChainId, WETH, Token } from '@uniswap/sdk'
import { useWeb3React } from '../hooks'
import { useLocalStorageTokens } from './LocalStorage'
export const ALL_TOKENS = [
// WETH on all chains
...Object.values(WETH),
// Mainnet Tokens
// Rinkeby Tokens
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.RINKEBY, '0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44', 18, 'IANV2', 'IAn V2 /Coin'),
new Token(ChainId.RINKEBY, '0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85', 18, 'MKR', 'Maker'),
// Kovan Tokens
new Token(ChainId.KOVAN, '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa', 18, 'DAI', 'Dai Stablecoin'),
// Ropsten Tokens
new Token(ChainId.ROPSTEN, '0xaD6D458402F60fD3Bd25163575031ACDce07538D', 18, 'DAI', 'Dai Stablecoin')
// Goerli Tokens
]
// put into an object
.reduce((tokenMap, token) => {
if (tokenMap?.[token.chainId]?.[token.address] !== undefined) throw Error('Duplicate tokens.')
return {
...tokenMap,
[token.chainId]: {
...tokenMap?.[token.chainId],
[token.address]: token
}
}
}, {})
export function useAllTokens(): { [address: string]: Token } {
const { chainId } = useWeb3React()
const [localStorageTokens] = useLocalStorageTokens()
return useMemo(() => {
return (
localStorageTokens
// filter to the current chain
.filter(token => token.chainId === chainId)
// reduce into all ALL_TOKENS filtered by the current chain
.reduce((tokenMap, token) => {
return {
...tokenMap,
[token.address]: token
}
}, ALL_TOKENS?.[chainId] ?? {})
)
}, [localStorageTokens, chainId])
}
export function useToken(tokenAddress: string): Token {
const tokens = useAllTokens()
const token = tokens?.[tokenAddress]
// rename WETH to ETH
if (token?.equals(WETH[token?.chainId])) {
;(token as any).symbol = 'ETH'
;(token as any).name = 'Ether'
}
return token
}
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
import { ChainId, WETH, Token } from '@uniswap/sdk'
import { useWeb3React } from '../hooks'
import { isAddress, getTokenName, getTokenSymbol, getTokenDecimals, safeAccess } from '../utils'
const UPDATE = 'UPDATE'
export let ALL_TOKENS = [
//Mainnet Tokens
WETH[ChainId.MAINNET],
// Rinkeby Tokens
WETH[ChainId.RINKEBY],
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.RINKEBY, '0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44', 18, 'IANV2', 'IAn V2 /Coin'),
new Token(ChainId.RINKEBY, '0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85', 18, 'MKR', 'Maker'),
//Kovan Tokens
WETH[ChainId.KOVAN],
new Token(ChainId.KOVAN, '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa', 18, 'DAI', 'Dai Stablecoin'),
//Ropsten Tokens
WETH[ChainId.ROPSTEN],
new Token(ChainId.ROPSTEN, '0xaD6D458402F60fD3Bd25163575031ACDce07538D', 18, 'DAI', 'Dai Stablecoin'),
//Goerli Tokens
WETH[ChainId.GÖRLI]
]
/**
* @todo is there a better way to load these upfront?
*/
// add any tokens from local storage
const savedList = window?.localStorage?.UNISWAP && JSON.parse(window?.localStorage?.UNISWAP)?.TOKEN_LIST
if (savedList) {
const newTokens = Object.keys(savedList).map(key => {
const token = savedList[key]
return new Token(token.chainId, token.address, token.decimals, token.symbol, token.name)
})
ALL_TOKENS = ALL_TOKENS.concat(newTokens)
}
// only meant to be used in exchanges.ts!
export const INITIAL_TOKENS_CONTEXT = ALL_TOKENS.reduce((tokenMap, token) => {
// ensure tokens are unique
if (tokenMap?.[token.chainId]?.[token.address] !== undefined) throw Error(`Duplicate token: ${token}`)
return {
...tokenMap,
[token.chainId]: {
...tokenMap?.[token.chainId],
[token.address]: token
}
}
}, {})
const TokensContext = createContext([])
function useTokensContext() {
return useContext(TokensContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case UPDATE: {
const { chainId, token } = payload
return {
...state,
[chainId]: {
...(state?.[chainId] || {}),
[token.address]: token
}
}
}
default: {
throw Error(`Unexpected action type in TokensContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, INITIAL_TOKENS_CONTEXT)
const update = useCallback((chainId, token) => {
dispatch({ type: UPDATE, payload: { chainId, token } })
}, [])
return (
<TokensContext.Provider value={useMemo(() => [state, { update }], [state, update])}>
{children}
</TokensContext.Provider>
)
}
export function useToken(tokenAddress: string): Token {
const { library, chainId } = useWeb3React()
const [state, { update }] = useTokensContext()
const allTokensInNetwork = state?.[chainId] || {}
const token = safeAccess(allTokensInNetwork, [tokenAddress])
useEffect(() => {
if (
isAddress(tokenAddress) &&
(token === null || token.name === undefined || token.symbol === undefined || token.decimals === undefined) &&
(chainId || chainId === 0) &&
library
) {
let stale = false
const namePromise = getTokenName(tokenAddress, library).catch(() => null)
const symbolPromise = getTokenSymbol(tokenAddress, library).catch(() => null)
const decimalsPromise = getTokenDecimals(tokenAddress, library).catch(() => null)
Promise.all([namePromise, symbolPromise, decimalsPromise]).then(
([resolvedName, resolvedSymbol, resolvedDecimals]) => {
if (!stale && resolvedDecimals) {
const newToken: Token = new Token(chainId, tokenAddress, resolvedDecimals, resolvedSymbol, resolvedName)
update(chainId, newToken)
}
}
)
return () => {
stale = true
}
}
}, [tokenAddress, token, chainId, library, update])
// hard coded change in UI to display WETH as ETH
if (token && token.name === 'WETH') {
token.name = 'ETH'
}
if (token && token.symbol === 'WETH') {
token.symbol = 'ETH'
}
return token
}
export function useAllTokens(): string[] {
const { chainId } = useWeb3React()
const [state] = useTokensContext()
return useMemo(() => {
// hardcode overide weth as ETH
if (state && state[chainId] && state[chainId][WETH[chainId].address]) {
state[chainId][WETH[chainId].address].symbol = 'ETH'
state[chainId][WETH[chainId].address].name = 'ETH'
}
return state?.[chainId] || {}
}, [state, chainId])
}
......@@ -6,11 +6,10 @@ import { ethers } from 'ethers'
import { NetworkContextName } from './constants'
import { isMobile } from 'react-device-detect'
import LocalStorageContextProvider, { Updater as LocalStorageContextUpdater } from './contexts/LocalStorage'
import LocalStorageContextProvider from './contexts/LocalStorage'
import ApplicationContextProvider, { Updater as ApplicationContextUpdater } from './contexts/Application'
import TransactionContextProvider, { Updater as TransactionContextUpdater } from './contexts/Transactions'
import BalancesContextProvider, { Updater as BalancesContextUpdater } from './contexts/Balances'
import TokensContextProvider from './contexts/Tokens'
import ExchangesContextProvider from './contexts/Pairs'
import AllowancesContextProvider from './contexts/Allowances'
import RoutesContextProvider from './contexts/Routes'
......@@ -44,11 +43,9 @@ function ContextProviders({ children }) {
<TransactionContextProvider>
<ExchangesContextProvider>
<RoutesContextProvider>
<TokensContextProvider>
<BalancesContextProvider>
<AllowancesContextProvider>{children}</AllowancesContextProvider>
</BalancesContextProvider>
</TokensContextProvider>
<BalancesContextProvider>
<AllowancesContextProvider>{children}</AllowancesContextProvider>
</BalancesContextProvider>
</RoutesContextProvider>
</ExchangesContextProvider>
</TransactionContextProvider>
......@@ -60,7 +57,6 @@ function ContextProviders({ children }) {
function Updaters() {
return (
<>
<LocalStorageContextUpdater />
<ApplicationContextUpdater />
<TransactionContextUpdater />
<BalancesContextUpdater />
......
......@@ -81,58 +81,26 @@ export default function App() {
{/* this Suspense is for route code-splitting */}
<Suspense fallback={null}>
<Switch>
<Route exact strict path="/" render={() => <Redirect to={{ pathname: '/swap' }} />} />
<Route exact strict path="/find" component={() => <Find params={params} />} />
<Route exact strict path="/create" component={() => <Create params={params} />} />
<Route exact strict path="/" render={() => <Redirect to="/swap" />} />
<Route exact strict path="/swap" component={() => <Swap params={params} />} />
<Route
exact
strict
path="/swap/:tokenAddress?"
render={({ match, location }) => {
if (isAddress(match.params.tokenAddress)) {
return (
<Swap
location={location}
initialCurrency={isAddress(match.params.tokenAddress)}
params={params}
/>
)
} else {
return <Redirect to={{ pathname: '/swap' }} />
}
}}
/>
<Route exact strict path="/send" component={() => <Send params={params} />} />
<Route
exact
strict
path="/send/:tokenAddress?"
render={({ match }) => {
if (isAddress(match.params.tokenAddress)) {
return <Send initialCurrency={isAddress(match.params.tokenAddress)} params={params} />
} else {
return <Redirect to={{ pathname: '/send' }} />
}
}}
/>
<Route exaxct path={'/pool'} component={() => <Pool params={params} />} />
<Route exact strict path="/find" component={() => <Find params={params} />} />
<Route exact strict path="/create" component={() => <Create params={params} />} />
<Route exact strict path="/pool" component={() => <Pool params={params} />} />
<Route
exact
strict
path={'/add/:tokens'}
component={({ match }) => {
const tokens = match.params.tokens.split('-')
let t0
let t1
if (tokens) {
t0 = tokens?.[0] === 'ETH' ? 'ETH' : isAddress(tokens[0])
t1 = tokens?.[1] === 'ETH' ? 'ETH' : isAddress(tokens[1])
}
const t0 =
tokens?.[0] === 'ETH' ? 'ETH' : isAddress(tokens?.[0]) ? isAddress(tokens[0]) : undefined
const t1 =
tokens?.[1] === 'ETH' ? 'ETH' : isAddress(tokens?.[1]) ? isAddress(tokens[1]) : undefined
if (t0 && t1) {
return <Add params={params} token0={t0} token1={t1} />
return <Add token0={t0} token1={t1} params={params} />
} else {
return <Redirect to={{ pathname: '/pool' }} />
return <Redirect to="/pool" />
}
}}
/>
......@@ -142,20 +110,18 @@ export default function App() {
path={'/remove/:tokens'}
component={({ match }) => {
const tokens = match.params.tokens.split('-')
let t0
let t1
if (tokens) {
t0 = tokens?.[0] === 'ETH' ? 'ETH' : isAddress(tokens[0])
t1 = tokens?.[1] === 'ETH' ? 'ETH' : isAddress(tokens[1])
}
const t0 =
tokens?.[0] === 'ETH' ? 'ETH' : isAddress(tokens?.[0]) ? isAddress(tokens[0]) : undefined
const t1 =
tokens?.[1] === 'ETH' ? 'ETH' : isAddress(tokens?.[1]) ? isAddress(tokens[1]) : undefined
if (t0 && t1) {
return <Remove params={params} token0={t0} token1={t1} />
return <Remove token0={t0} token1={t1} params={params} />
} else {
return <Redirect to={{ pathname: '/pool' }} />
return <Redirect to="/pool" />
}
}}
/>
<Route exaxct path={'/remove'} component={() => <Remove params={params} />} />
<Redirect to="/" />
</Switch>
</Suspense>
</BrowserRouter>
......
......@@ -28,7 +28,7 @@ import { useTransactionAdder, usePendingApproval } from '../../contexts/Transact
import { BigNumber } from 'ethers/utils'
import { ROUTER_ADDRESSES } from '../../constants'
import { getRouterContract, calculateGasMargin } from '../../utils'
import { getRouterContract, calculateGasMargin, isWETH } from '../../utils'
// denominated in bips
const ALLOWED_SLIPPAGE = 50
......@@ -293,11 +293,8 @@ function AddLiquidity({ token0, token1, step = false }) {
const [maxAmountInput, maxAmountOutput]: TokenAmount[] = [Field.INPUT, Field.OUTPUT].map(index => {
const field = Field[index]
return !!userBalances[Field[field]] &&
JSBI.greaterThan(
userBalances[Field[field]].raw,
tokens[Field[field]].equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
)
? tokens[Field[field]].equals(WETH[chainId])
JSBI.greaterThan(userBalances[Field[field]].raw, isWETH(tokens[Field[field]]) ? MIN_ETHER.raw : JSBI.BigInt(0))
? isWETH(tokens[Field[field]])
? userBalances[Field[field]].subtract(MIN_ETHER)
: userBalances[Field[field]]
: undefined
......
import React from 'react'
import ExchangePage from '../../components/ExchangePage'
export default function Swap({ initialCurrency, params }) {
return <ExchangePage sendingInput={false} initialCurrency={initialCurrency} params={params} />
export default function Swap({ params }) {
return <ExchangePage sendingInput={false} params={params} />
}
......@@ -169,14 +169,9 @@ export const TYPE = {
}
export const GlobalStyle = createGlobalStyle`
@import url('https://rsms.me/inter/inter.css');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@531&display=swap');
html {
font-family: 'Inter', sans-serif;
letter-spacing: -0.018em;
font-feature-settings: 'cv01', 'cv02', 'cv03', 'cv04';
}
@supports (font-variation-settings: normal) {
html { font-family: 'Inter var', sans-serif; }
}
html,
......
......@@ -9,6 +9,7 @@ import { FACTORY_ADDRESSES, SUPPORTED_THEMES, ROUTER_ADDRESSES } from '../consta
import { bigNumberify, keccak256, defaultAbiCoder, toUtf8Bytes, solidityPack } from 'ethers/utils'
import UncheckedJsonRpcSigner from './signer'
import { WETH } from '@uniswap/sdk'
export const ERROR_CODES = ['TOKEN_NAME', 'TOKEN_SYMBOL', 'TOKEN_DECIMALS'].reduce(
(accumulator, currentValue, currentIndex) => {
......@@ -65,11 +66,11 @@ export function getAllQueryParams() {
? isAddress(getQueryParam(window.location, 'outputTokenAddress'))
: ''
params.inputTokenAmount = !isNaN(getQueryParam(window.location, 'inputTokenAmount'))
params.inputTokenAmount = !isNaN(Number(getQueryParam(window.location, 'inputTokenAmount')))
? getQueryParam(window.location, 'inputTokenAmount')
: ''
params.outputTokenAmount = !isNaN(getQueryParam(window.location, 'outputTokenAmount'))
params.outputTokenAmount = !isNaN(Number(getQueryParam(window.location, 'outputTokenAmount')))
? getQueryParam(window.location, 'outputTokenAmount')
: ''
......@@ -86,7 +87,7 @@ export function checkSupportedTheme(themeName) {
export function getNetworkName(networkId) {
switch (networkId) {
case 1: {
return 'the Main Ethereum Network'
return 'the Ethereum Mainnet'
}
case 3: {
return 'the Ropsten Test Network'
......@@ -291,3 +292,11 @@ export async function getApprovalDigest(token, approve, nonce, deadline) {
)
)
}
export function isWETH(token) {
if (token && token.address === WETH[token.chainId].address) {
return true
} else {
return false
}
}
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