import { Percent, Token } from '@uniswap/sdk-core' import { computePairAddress, Pair } from '@uniswap/v2-sdk' import { L2_CHAIN_IDS } from 'constants/chains' import { SupportedLocale } from 'constants/locales' import { L2_DEADLINE_FROM_NOW } from 'constants/misc' import JSBI from 'jsbi' import { useCallback, useMemo } from 'react' import { shallowEqual } from 'react-redux' import { useAppDispatch, useAppSelector } from 'state/hooks' import { V2_FACTORY_ADDRESSES } from '../../constants/addresses' import { BASES_TO_TRACK_LIQUIDITY_FOR, PINNED_PAIRS } from '../../constants/routing' import { useAllTokens } from '../../hooks/Tokens' import { useActiveWeb3React } from '../../hooks/web3' import { AppState } from '../index' import { addSerializedPair, addSerializedToken, removeSerializedToken, SerializedPair, SerializedToken, updateArbitrumAlphaAcknowledged, updateHideClosedPositions, updateUserDarkMode, updateUserDeadline, updateUserExpertMode, updateUserLocale, updateUserSingleHopOnly, updateUserSlippageTolerance, } from './actions' function serializeToken(token: Token): SerializedToken { return { chainId: token.chainId, address: token.address, decimals: token.decimals, symbol: token.symbol, name: token.name, } } function deserializeToken(serializedToken: SerializedToken): Token { return new Token( serializedToken.chainId, serializedToken.address, serializedToken.decimals, serializedToken.symbol, serializedToken.name ) } export function useIsDarkMode(): boolean { const { userDarkMode, matchesDarkMode } = useAppSelector( ({ user: { matchesDarkMode, userDarkMode } }) => ({ userDarkMode, matchesDarkMode, }), shallowEqual ) return userDarkMode === null ? matchesDarkMode : userDarkMode } export function useDarkModeManager(): [boolean, () => void] { const dispatch = useAppDispatch() const darkMode = useIsDarkMode() const toggleSetDarkMode = useCallback(() => { dispatch(updateUserDarkMode({ userDarkMode: !darkMode })) }, [darkMode, dispatch]) return [darkMode, toggleSetDarkMode] } export function useUserLocale(): SupportedLocale | null { return useAppSelector((state) => state.user.userLocale) } export function useUserLocaleManager(): [SupportedLocale | null, (newLocale: SupportedLocale) => void] { const dispatch = useAppDispatch() const locale = useUserLocale() const setLocale = useCallback( (newLocale: SupportedLocale) => { dispatch(updateUserLocale({ userLocale: newLocale })) }, [dispatch] ) return [locale, setLocale] } export function useIsExpertMode(): boolean { return useAppSelector((state) => state.user.userExpertMode) } export function useExpertModeManager(): [boolean, () => void] { const dispatch = useAppDispatch() const expertMode = useIsExpertMode() const toggleSetExpertMode = useCallback(() => { dispatch(updateUserExpertMode({ userExpertMode: !expertMode })) }, [expertMode, dispatch]) return [expertMode, toggleSetExpertMode] } export function useUserSingleHopOnly(): [boolean, (newSingleHopOnly: boolean) => void] { const dispatch = useAppDispatch() const singleHopOnly = useAppSelector((state) => state.user.userSingleHopOnly) const setSingleHopOnly = useCallback( (newSingleHopOnly: boolean) => { dispatch(updateUserSingleHopOnly({ userSingleHopOnly: newSingleHopOnly })) }, [dispatch] ) return [singleHopOnly, setSingleHopOnly] } export function useSetUserSlippageTolerance(): (slippageTolerance: Percent | 'auto') => void { const dispatch = useAppDispatch() return useCallback( (userSlippageTolerance: Percent | 'auto') => { let value: 'auto' | number try { value = userSlippageTolerance === 'auto' ? 'auto' : JSBI.toNumber(userSlippageTolerance.multiply(10_000).quotient) } catch (error) { value = 'auto' } dispatch( updateUserSlippageTolerance({ userSlippageTolerance: value, }) ) }, [dispatch] ) } /** * Return the user's slippage tolerance, from the redux store, and a function to update the slippage tolerance */ export function useUserSlippageTolerance(): Percent | 'auto' { const userSlippageTolerance = useAppSelector((state) => { return state.user.userSlippageTolerance }) return useMemo( () => (userSlippageTolerance === 'auto' ? 'auto' : new Percent(userSlippageTolerance, 10_000)), [userSlippageTolerance] ) } export function useUserHideClosedPositions(): [boolean, (newHideClosedPositions: boolean) => void] { const dispatch = useAppDispatch() const hideClosedPositions = useAppSelector((state) => state.user.userHideClosedPositions) const setHideClosedPositions = useCallback( (newHideClosedPositions: boolean) => { dispatch(updateHideClosedPositions({ userHideClosedPositions: newHideClosedPositions })) }, [dispatch] ) return [hideClosedPositions, setHideClosedPositions] } /** * Same as above but replaces the auto with a default value * @param defaultSlippageTolerance the default value to replace auto with */ export function useUserSlippageToleranceWithDefault(defaultSlippageTolerance: Percent): Percent { const allowedSlippage = useUserSlippageTolerance() return useMemo( () => (allowedSlippage === 'auto' ? defaultSlippageTolerance : allowedSlippage), [allowedSlippage, defaultSlippageTolerance] ) } export function useUserTransactionTTL(): [number, (slippage: number) => void] { const { chainId } = useActiveWeb3React() const dispatch = useAppDispatch() const userDeadline = useAppSelector((state) => state.user.userDeadline) const onL2 = Boolean(chainId && L2_CHAIN_IDS.includes(chainId)) const deadline = onL2 ? L2_DEADLINE_FROM_NOW : userDeadline const setUserDeadline = useCallback( (userDeadline: number) => { dispatch(updateUserDeadline({ userDeadline })) }, [dispatch] ) return [deadline, setUserDeadline] } export function useAddUserToken(): (token: Token) => void { const dispatch = useAppDispatch() return useCallback( (token: Token) => { dispatch(addSerializedToken({ serializedToken: serializeToken(token) })) }, [dispatch] ) } export function useRemoveUserAddedToken(): (chainId: number, address: string) => void { const dispatch = useAppDispatch() return useCallback( (chainId: number, address: string) => { dispatch(removeSerializedToken({ chainId, address })) }, [dispatch] ) } export function useUserAddedTokens(): Token[] { const { chainId } = useActiveWeb3React() const serializedTokensMap = useAppSelector(({ user: { tokens } }) => tokens) return useMemo(() => { if (!chainId) return [] return Object.values(serializedTokensMap?.[chainId] ?? {}).map(deserializeToken) }, [serializedTokensMap, chainId]) } function serializePair(pair: Pair): SerializedPair { return { token0: serializeToken(pair.token0), token1: serializeToken(pair.token1), } } export function usePairAdder(): (pair: Pair) => void { const dispatch = useAppDispatch() return useCallback( (pair: Pair) => { dispatch(addSerializedPair({ serializedPair: serializePair(pair) })) }, [dispatch] ) } export function useURLWarningVisible(): boolean { return useAppSelector((state: AppState) => state.user.URLWarningVisible) } /** * Given two tokens return the liquidity token that represents its liquidity shares * @param tokenA one of the two tokens * @param tokenB the other token */ export function toV2LiquidityToken([tokenA, tokenB]: [Token, Token]): Token { if (tokenA.chainId !== tokenB.chainId) throw new Error('Not matching chain IDs') if (tokenA.equals(tokenB)) throw new Error('Tokens cannot be equal') if (!V2_FACTORY_ADDRESSES[tokenA.chainId]) throw new Error('No V2 factory address on this chain') return new Token( tokenA.chainId, computePairAddress({ factoryAddress: V2_FACTORY_ADDRESSES[tokenA.chainId], tokenA, tokenB }), 18, 'UNI-V2', 'Uniswap V2' ) } /** * Returns all the pairs of tokens that are tracked by the user for the current chain ID. */ export function useTrackedTokenPairs(): [Token, Token][] { const { chainId } = useActiveWeb3React() const tokens = useAllTokens() // pinned pairs const pinnedPairs = useMemo(() => (chainId ? PINNED_PAIRS[chainId] ?? [] : []), [chainId]) // pairs for every token against every base const generatedPairs: [Token, Token][] = useMemo( () => chainId ? Object.keys(tokens).flatMap((tokenAddress) => { const token = tokens[tokenAddress] // for each token on the current chain, return ( // loop though all bases on the current chain (BASES_TO_TRACK_LIQUIDITY_FOR[chainId] ?? []) // to construct pairs of the given token with each base .map((base) => { if (base.address === token.address) { return null } else { return [base, token] } }) .filter((p): p is [Token, Token] => p !== null) ) }) : [], [tokens, chainId] ) // pairs saved by users const savedSerializedPairs = useAppSelector(({ user: { pairs } }) => pairs) const userPairs: [Token, Token][] = useMemo(() => { if (!chainId || !savedSerializedPairs) return [] const forChain = savedSerializedPairs[chainId] if (!forChain) return [] return Object.keys(forChain).map((pairId) => { return [deserializeToken(forChain[pairId].token0), deserializeToken(forChain[pairId].token1)] }) }, [savedSerializedPairs, chainId]) const combinedList = useMemo( () => userPairs.concat(generatedPairs).concat(pinnedPairs), [generatedPairs, pinnedPairs, userPairs] ) return useMemo(() => { // dedupes pairs of tokens in the combined list const keyed = combinedList.reduce<{ [key: string]: [Token, Token] }>((memo, [tokenA, tokenB]) => { const sorted = tokenA.sortsBefore(tokenB) const key = sorted ? `${tokenA.address}:${tokenB.address}` : `${tokenB.address}:${tokenA.address}` if (memo[key]) return memo memo[key] = sorted ? [tokenA, tokenB] : [tokenB, tokenA] return memo }, {}) return Object.keys(keyed).map((key) => keyed[key]) }, [combinedList]) } export function useArbitrumAlphaAlert(): [boolean, (arbitrumAlphaAcknowledged: boolean) => void] { const dispatch = useAppDispatch() const arbitrumAlphaAcknowledged = useAppSelector(({ user }) => user.arbitrumAlphaAcknowledged) const setArbitrumAlphaAcknowledged = (arbitrumAlphaAcknowledged: boolean) => { dispatch(updateArbitrumAlphaAcknowledged({ arbitrumAlphaAcknowledged })) } return [arbitrumAlphaAcknowledged, setArbitrumAlphaAcknowledged] }