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", "name": "uniswap",
"description": "Uniswap Exchange Protocol", "description": "Uniswap Protocol",
"version": "0.1.0", "version": "0.1.0",
"homepage": "https://uniswap.exchange", "homepage": "https://uniswap.exchange",
"private": true, "private": true,
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
work correctly both with client-side routing and a non-root public URL. 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`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>Uniswap Exchange</title> <title>Uniswap</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
......
{ {
"short_name": "Uniswap", "short_name": "Uniswap",
"name": "Uniswap Exchange", "name": "Uniswap",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
......
...@@ -86,7 +86,7 @@ export default function AdvancedSettings({ setIsOpen, setDeadline, allowedSlippa ...@@ -86,7 +86,7 @@ export default function AdvancedSettings({ setIsOpen, setDeadline, allowedSlippa
back back
</Link> </Link>
<RowBetween> <RowBetween>
<TYPE.main>Limit front-running tolerance</TYPE.main> <TYPE.main>Front-running tolerance</TYPE.main>
<QuestionHelper text={t('toleranceExplanation')} /> <QuestionHelper text={t('toleranceExplanation')} />
</RowBetween> </RowBetween>
<Row> <Row>
......
...@@ -25,7 +25,7 @@ import { ButtonPrimary, ButtonError, ButtonLight } from '../Button' ...@@ -25,7 +25,7 @@ import { ButtonPrimary, ButtonError, ButtonLight } from '../Button'
import { GreyCard, BlueCard, YellowCard, LightCard } from '../../components/Card' import { GreyCard, BlueCard, YellowCard, LightCard } from '../../components/Card'
import { usePair } from '../../contexts/Pairs' import { usePair } from '../../contexts/Pairs'
import { useToken } from '../../contexts/Tokens' import { useToken, useAllTokens } from '../../contexts/Tokens'
import { useRoute } from '../../contexts/Routes' import { useRoute } from '../../contexts/Routes'
import { useAddressAllowance } from '../../contexts/Allowances' import { useAddressAllowance } from '../../contexts/Allowances'
import { useWeb3React, useTokenContract } from '../../hooks' import { useWeb3React, useTokenContract } from '../../hooks'
...@@ -33,8 +33,8 @@ import { useAddressBalance, useAllBalances } from '../../contexts/Balances' ...@@ -33,8 +33,8 @@ import { useAddressBalance, useAllBalances } from '../../contexts/Balances'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions' import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
import { ROUTER_ADDRESSES } from '../../constants' import { ROUTER_ADDRESSES } from '../../constants'
// import { INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens' import { getRouterContract, calculateGasMargin, getProviderOrSigner, getEtherscanLink, isWETH } from '../../utils'
import { getRouterContract, calculateGasMargin, getProviderOrSigner, getEtherscanLink } from '../../utils' import { useLocalStorageTokens } from '../../contexts/LocalStorage'
const Wrapper = styled.div` const Wrapper = styled.div`
position: relative; position: relative;
...@@ -286,7 +286,7 @@ const DEFAULT_DEADLINE_FROM_NOW = 60 * 20 ...@@ -286,7 +286,7 @@ const DEFAULT_DEADLINE_FROM_NOW = 60 * 20
const ALLOWED_SLIPPAGE_MEDIUM = 100 const ALLOWED_SLIPPAGE_MEDIUM = 100
const ALLOWED_SLIPPAGE_HIGH = 500 const ALLOWED_SLIPPAGE_HIGH = 500
function ExchangePage({ sendingInput = false, history, initialCurrency, params }) { function ExchangePage({ sendingInput = false, history, params }) {
// text translation // text translation
// const { t } = useTranslation() // const { t } = useTranslation()
...@@ -307,11 +307,7 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params } ...@@ -307,11 +307,7 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params }
reducer, reducer,
{ {
independentField: params.outputTokenAddress && !params.inputTokenAddress ? Field.OUTPUT : Field.INPUT, independentField: params.outputTokenAddress && !params.inputTokenAddress ? Field.OUTPUT : Field.INPUT,
inputTokenAddress: params.inputTokenAddress inputTokenAddress: params.inputTokenAddress ? params.inputTokenAddress : WETH[chainId].address,
? params.inputTokenAddress
: initialCurrency
? initialCurrency
: WETH[chainId].address,
outputTokenAddress: params.outputTokenAddress ? params.outputTokenAddress : '', outputTokenAddress: params.outputTokenAddress ? params.outputTokenAddress : '',
typedValue: typedValue:
params.inputTokenAddress && !params.outputTokenAddress params.inputTokenAddress && !params.outputTokenAddress
...@@ -340,6 +336,30 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params } ...@@ -340,6 +336,30 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params }
[Field.OUTPUT]: useToken(fieldData[Field.OUTPUT].address) [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 // token contracts for approvals and direct sends
const tokenContractInput: ethers.Contract = useTokenContract(tokens[Field.INPUT]?.address) const tokenContractInput: ethers.Contract = useTokenContract(tokens[Field.INPUT]?.address)
const tokenContractOutput: ethers.Contract = useTokenContract(tokens[Field.OUTPUT]?.address) const tokenContractOutput: ethers.Contract = useTokenContract(tokens[Field.OUTPUT]?.address)
...@@ -347,11 +367,6 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params } ...@@ -347,11 +367,6 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params }
// check on pending approvals for token amounts // check on pending approvals for token amounts
const pendingApprovalInput = usePendingApproval(tokens[Field.INPUT]?.address) 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 // entities for swap
const pair: Pair = usePair(tokens[Field.INPUT], tokens[Field.OUTPUT]) const pair: Pair = usePair(tokens[Field.INPUT], tokens[Field.OUTPUT])
const route = useRoute(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 } ...@@ -371,7 +386,7 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params }
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW) const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE) const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
// approvals // input approval
const inputApproval: TokenAmount = useAddressAllowance(account, tokens[Field.INPUT], routerAddress) const inputApproval: TokenAmount = useAddressAllowance(account, tokens[Field.INPUT], routerAddress)
// all balances for detecting a swap with send // all balances for detecting a swap with send
...@@ -477,11 +492,8 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params } ...@@ -477,11 +492,8 @@ function ExchangePage({ sendingInput = false, history, initialCurrency, params }
!!userBalances[Field.INPUT] && !!userBalances[Field.INPUT] &&
!!tokens[Field.INPUT] && !!tokens[Field.INPUT] &&
WETH[chainId] && WETH[chainId] &&
JSBI.greaterThan( JSBI.greaterThan(userBalances[Field.INPUT].raw, isWETH(tokens[Field.INPUT]) ? MIN_ETHER.raw : JSBI.BigInt(0))
userBalances[Field.INPUT].raw, ? isWETH(tokens[Field.INPUT])
tokens[Field.INPUT].equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
)
? tokens[Field.INPUT].equals(WETH[chainId])
? userBalances[Field.INPUT].subtract(MIN_ETHER) ? userBalances[Field.INPUT].subtract(MIN_ETHER)
: userBalances[Field.INPUT] : userBalances[Field.INPUT]
: undefined : undefined
......
This diff is collapsed.
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 React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect, useState } from 'react'
import { useAddressBalance } from './Balances' import { useAddressBalance } from './Balances'
import { useWeb3React, usePairContract } from '../hooks' 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' import { ChainId, WETH, Token, TokenAmount, Pair, JSBI } from '@uniswap/sdk'
const ADDRESSES_KEY = 'ADDRESSES_KEY' const ADDRESSES_KEY = 'ADDRESSES_KEY'
...@@ -12,12 +12,12 @@ const UPDATE_PAIR_ENTITY = 'UPDATE_PAIR_ENTITY' ...@@ -12,12 +12,12 @@ const UPDATE_PAIR_ENTITY = 'UPDATE_PAIR_ENTITY'
const ALL_PAIRS: [Token, Token][] = [ const ALL_PAIRS: [Token, Token][] = [
[ [
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY][WETH[ChainId.RINKEBY].address], ALL_TOKENS[ChainId.RINKEBY][WETH[ChainId.RINKEBY].address],
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'] //dai ALL_TOKENS[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'] //dai
], ],
[ [
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'], // dai ALL_TOKENS[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'], // dai
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44'] // mkr ALL_TOKENS[ChainId.RINKEBY]['0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44'] // mkr
] ]
] ]
......
...@@ -2,6 +2,7 @@ import React, { createContext, useContext, useReducer, useMemo, useCallback, use ...@@ -2,6 +2,7 @@ import React, { createContext, useContext, useReducer, useMemo, useCallback, use
import { WETH, Token, Route, JSBI } from '@uniswap/sdk' import { WETH, Token, Route, JSBI } from '@uniswap/sdk'
import { useWeb3React } from '../hooks' import { useWeb3React } from '../hooks'
import { usePair } from '../contexts/Pairs' import { usePair } from '../contexts/Pairs'
import { isWETH } from '../utils'
const UPDATE = 'UPDATE' const UPDATE = 'UPDATE'
...@@ -77,8 +78,8 @@ export function useRoute(tokenA: Token, tokenB: Token) { ...@@ -77,8 +78,8 @@ export function useRoute(tokenA: Token, tokenB: Token) {
const defaultPair = usePair(tokenA, tokenB) const defaultPair = usePair(tokenA, tokenB)
// get token<->WETH pairs // get token<->WETH pairs
const aToETH = usePair(tokenA && !tokenA.equals(WETH[chainId]) ? tokenA : null, WETH[chainId]) const aToETH = usePair(tokenA && !isWETH(tokenA) ? tokenA : null, WETH[chainId])
const bToETH = usePair(tokenB && !tokenB.equals(WETH[chainId]) ? tokenB : null, WETH[chainId]) const bToETH = usePair(tokenB && !isWETH(tokenB) ? tokenB : null, WETH[chainId])
// needs to route through WETH // needs to route through WETH
const requiresHop = 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' ...@@ -6,11 +6,10 @@ import { ethers } from 'ethers'
import { NetworkContextName } from './constants' import { NetworkContextName } from './constants'
import { isMobile } from 'react-device-detect' 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 ApplicationContextProvider, { Updater as ApplicationContextUpdater } from './contexts/Application'
import TransactionContextProvider, { Updater as TransactionContextUpdater } from './contexts/Transactions' import TransactionContextProvider, { Updater as TransactionContextUpdater } from './contexts/Transactions'
import BalancesContextProvider, { Updater as BalancesContextUpdater } from './contexts/Balances' import BalancesContextProvider, { Updater as BalancesContextUpdater } from './contexts/Balances'
import TokensContextProvider from './contexts/Tokens'
import ExchangesContextProvider from './contexts/Pairs' import ExchangesContextProvider from './contexts/Pairs'
import AllowancesContextProvider from './contexts/Allowances' import AllowancesContextProvider from './contexts/Allowances'
import RoutesContextProvider from './contexts/Routes' import RoutesContextProvider from './contexts/Routes'
...@@ -44,11 +43,9 @@ function ContextProviders({ children }) { ...@@ -44,11 +43,9 @@ function ContextProviders({ children }) {
<TransactionContextProvider> <TransactionContextProvider>
<ExchangesContextProvider> <ExchangesContextProvider>
<RoutesContextProvider> <RoutesContextProvider>
<TokensContextProvider> <BalancesContextProvider>
<BalancesContextProvider> <AllowancesContextProvider>{children}</AllowancesContextProvider>
<AllowancesContextProvider>{children}</AllowancesContextProvider> </BalancesContextProvider>
</BalancesContextProvider>
</TokensContextProvider>
</RoutesContextProvider> </RoutesContextProvider>
</ExchangesContextProvider> </ExchangesContextProvider>
</TransactionContextProvider> </TransactionContextProvider>
...@@ -60,7 +57,6 @@ function ContextProviders({ children }) { ...@@ -60,7 +57,6 @@ function ContextProviders({ children }) {
function Updaters() { function Updaters() {
return ( return (
<> <>
<LocalStorageContextUpdater />
<ApplicationContextUpdater /> <ApplicationContextUpdater />
<TransactionContextUpdater /> <TransactionContextUpdater />
<BalancesContextUpdater /> <BalancesContextUpdater />
......
...@@ -81,58 +81,26 @@ export default function App() { ...@@ -81,58 +81,26 @@ export default function App() {
{/* this Suspense is for route code-splitting */} {/* this Suspense is for route code-splitting */}
<Suspense fallback={null}> <Suspense fallback={null}>
<Switch> <Switch>
<Route exact strict path="/" render={() => <Redirect to={{ pathname: '/swap' }} />} /> <Route exact strict path="/" render={() => <Redirect to="/swap" />} />
<Route exact strict path="/find" component={() => <Find params={params} />} />
<Route exact strict path="/create" component={() => <Create params={params} />} />
<Route exact strict path="/swap" component={() => <Swap params={params} />} /> <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" component={() => <Send params={params} />} />
<Route <Route exact strict path="/find" component={() => <Find params={params} />} />
exact <Route exact strict path="/create" component={() => <Create params={params} />} />
strict <Route exact strict path="/pool" component={() => <Pool params={params} />} />
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 <Route
exact exact
strict strict
path={'/add/:tokens'} path={'/add/:tokens'}
component={({ match }) => { component={({ match }) => {
const tokens = match.params.tokens.split('-') const tokens = match.params.tokens.split('-')
let t0 const t0 =
let t1 tokens?.[0] === 'ETH' ? 'ETH' : isAddress(tokens?.[0]) ? isAddress(tokens[0]) : undefined
if (tokens) { const t1 =
t0 = tokens?.[0] === 'ETH' ? 'ETH' : isAddress(tokens[0]) tokens?.[1] === 'ETH' ? 'ETH' : isAddress(tokens?.[1]) ? isAddress(tokens[1]) : undefined
t1 = tokens?.[1] === 'ETH' ? 'ETH' : isAddress(tokens[1])
}
if (t0 && t1) { if (t0 && t1) {
return <Add params={params} token0={t0} token1={t1} /> return <Add token0={t0} token1={t1} params={params} />
} else { } else {
return <Redirect to={{ pathname: '/pool' }} /> return <Redirect to="/pool" />
} }
}} }}
/> />
...@@ -142,20 +110,18 @@ export default function App() { ...@@ -142,20 +110,18 @@ export default function App() {
path={'/remove/:tokens'} path={'/remove/:tokens'}
component={({ match }) => { component={({ match }) => {
const tokens = match.params.tokens.split('-') const tokens = match.params.tokens.split('-')
let t0 const t0 =
let t1 tokens?.[0] === 'ETH' ? 'ETH' : isAddress(tokens?.[0]) ? isAddress(tokens[0]) : undefined
if (tokens) { const t1 =
t0 = tokens?.[0] === 'ETH' ? 'ETH' : isAddress(tokens[0]) tokens?.[1] === 'ETH' ? 'ETH' : isAddress(tokens?.[1]) ? isAddress(tokens[1]) : undefined
t1 = tokens?.[1] === 'ETH' ? 'ETH' : isAddress(tokens[1])
}
if (t0 && t1) { if (t0 && t1) {
return <Remove params={params} token0={t0} token1={t1} /> return <Remove token0={t0} token1={t1} params={params} />
} else { } else {
return <Redirect to={{ pathname: '/pool' }} /> return <Redirect to="/pool" />
} }
}} }}
/> />
<Route exaxct path={'/remove'} component={() => <Remove params={params} />} /> <Redirect to="/" />
</Switch> </Switch>
</Suspense> </Suspense>
</BrowserRouter> </BrowserRouter>
......
...@@ -28,7 +28,7 @@ import { useTransactionAdder, usePendingApproval } from '../../contexts/Transact ...@@ -28,7 +28,7 @@ import { useTransactionAdder, usePendingApproval } from '../../contexts/Transact
import { BigNumber } from 'ethers/utils' import { BigNumber } from 'ethers/utils'
import { ROUTER_ADDRESSES } from '../../constants' import { ROUTER_ADDRESSES } from '../../constants'
import { getRouterContract, calculateGasMargin } from '../../utils' import { getRouterContract, calculateGasMargin, isWETH } from '../../utils'
// denominated in bips // denominated in bips
const ALLOWED_SLIPPAGE = 50 const ALLOWED_SLIPPAGE = 50
...@@ -293,11 +293,8 @@ function AddLiquidity({ token0, token1, step = false }) { ...@@ -293,11 +293,8 @@ function AddLiquidity({ token0, token1, step = false }) {
const [maxAmountInput, maxAmountOutput]: TokenAmount[] = [Field.INPUT, Field.OUTPUT].map(index => { const [maxAmountInput, maxAmountOutput]: TokenAmount[] = [Field.INPUT, Field.OUTPUT].map(index => {
const field = Field[index] const field = Field[index]
return !!userBalances[Field[field]] && return !!userBalances[Field[field]] &&
JSBI.greaterThan( JSBI.greaterThan(userBalances[Field[field]].raw, isWETH(tokens[Field[field]]) ? MIN_ETHER.raw : JSBI.BigInt(0))
userBalances[Field[field]].raw, ? isWETH(tokens[Field[field]])
tokens[Field[field]].equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
)
? tokens[Field[field]].equals(WETH[chainId])
? userBalances[Field[field]].subtract(MIN_ETHER) ? userBalances[Field[field]].subtract(MIN_ETHER)
: userBalances[Field[field]] : userBalances[Field[field]]
: undefined : undefined
......
import React from 'react' import React from 'react'
import ExchangePage from '../../components/ExchangePage' import ExchangePage from '../../components/ExchangePage'
export default function Swap({ initialCurrency, params }) { export default function Swap({ params }) {
return <ExchangePage sendingInput={false} initialCurrency={initialCurrency} params={params} /> return <ExchangePage sendingInput={false} params={params} />
} }
...@@ -169,14 +169,9 @@ export const TYPE = { ...@@ -169,14 +169,9 @@ export const TYPE = {
} }
export const GlobalStyle = createGlobalStyle` 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 { html {
font-family: 'Inter', sans-serif; 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, html,
......
...@@ -9,6 +9,7 @@ import { FACTORY_ADDRESSES, SUPPORTED_THEMES, ROUTER_ADDRESSES } from '../consta ...@@ -9,6 +9,7 @@ import { FACTORY_ADDRESSES, SUPPORTED_THEMES, ROUTER_ADDRESSES } from '../consta
import { bigNumberify, keccak256, defaultAbiCoder, toUtf8Bytes, solidityPack } from 'ethers/utils' import { bigNumberify, keccak256, defaultAbiCoder, toUtf8Bytes, solidityPack } from 'ethers/utils'
import UncheckedJsonRpcSigner from './signer' import UncheckedJsonRpcSigner from './signer'
import { WETH } from '@uniswap/sdk'
export const ERROR_CODES = ['TOKEN_NAME', 'TOKEN_SYMBOL', 'TOKEN_DECIMALS'].reduce( export const ERROR_CODES = ['TOKEN_NAME', 'TOKEN_SYMBOL', 'TOKEN_DECIMALS'].reduce(
(accumulator, currentValue, currentIndex) => { (accumulator, currentValue, currentIndex) => {
...@@ -65,11 +66,11 @@ export function getAllQueryParams() { ...@@ -65,11 +66,11 @@ export function getAllQueryParams() {
? isAddress(getQueryParam(window.location, 'outputTokenAddress')) ? isAddress(getQueryParam(window.location, 'outputTokenAddress'))
: '' : ''
params.inputTokenAmount = !isNaN(getQueryParam(window.location, 'inputTokenAmount')) params.inputTokenAmount = !isNaN(Number(getQueryParam(window.location, 'inputTokenAmount')))
? 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') ? getQueryParam(window.location, 'outputTokenAmount')
: '' : ''
...@@ -86,7 +87,7 @@ export function checkSupportedTheme(themeName) { ...@@ -86,7 +87,7 @@ export function checkSupportedTheme(themeName) {
export function getNetworkName(networkId) { export function getNetworkName(networkId) {
switch (networkId) { switch (networkId) {
case 1: { case 1: {
return 'the Main Ethereum Network' return 'the Ethereum Mainnet'
} }
case 3: { case 3: {
return 'the Ropsten Test Network' return 'the Ropsten Test Network'
...@@ -291,3 +292,11 @@ export async function getApprovalDigest(token, approve, nonce, deadline) { ...@@ -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