diff --git a/src/apollo/queries.ts b/src/apollo/queries.ts index addf30d2c..984fd4bb3 100755 --- a/src/apollo/queries.ts +++ b/src/apollo/queries.ts @@ -18,6 +18,94 @@ export const SUBGRAPH_HEALTH = gql` } `; +export const TOKEN_SEARCH = gql` + query tokens($value: String, $id: String) { + asSymbol: tokens( + where: { symbol_contains: $value } + orderBy: totalLiquidity + orderDirection: desc + ) { + id + symbol + name + decimals + totalLiquidity + } + asName: tokens( + where: { name_contains: $value } + orderBy: totalLiquidity + orderDirection: desc + ) { + id + symbol + name + decimals + totalLiquidity + } + asAddress: tokens( + where: { id: $id } + orderBy: totalLiquidity + orderDirection: desc + ) { + id + symbol + name + decimals + totalLiquidity + } + } +`; + +export const PAIR_SEARCH = gql` + query pairs($tokens: [Bytes]!, $id: String) { + as0: pairs(where: { token0_in: $tokens }) { + id + token0 { + id + symbol + decimals + name + } + token1 { + id + symbol + decimals + name + } + } + as1: pairs(where: { token1_in: $tokens }) { + id + token0 { + id + symbol + decimals + name + } + token1 { + id + symbol + decimals + name + } + } + asAddress: pairs(where: { id: $id }) { + id + token0 { + id + symbol + decimals + name + } + token1 { + id + symbol + decimals + name + } + } + } +`; + export const TOKEN_CHART = gql` query tokenDayDatas($tokenAddr: String!, $startTime: Int!) { tokenDayDatas( @@ -86,6 +174,43 @@ export const PAIRS_BULK: any = (pairs: any[]) => { return gql(queryString); }; +export const ALL_TOKENS = gql` + query tokens($skip: Int!) { + tokens(first: 10, skip: $skip) { + id + name + symbol + decimals + totalLiquidity + } + } +`; + +export const ALL_PAIRS = gql` + query pairs($skip: Int!) { + pairs( + first: 10 + skip: $skip + orderBy: trackedReserveETH + orderDirection: desc + ) { + id + token0 { + id + symbol + name + decimals + } + token1 { + id + symbol + name + decimals + } + } + } +`; + export const PAIRS_BULK1 = gql` ${PairFields} query pairs($allPairs: [Bytes]!) { diff --git a/src/constants/index.ts b/src/constants/index.ts index 742f39bd3..580e8e1c8 100755 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -17,6 +17,19 @@ import { } from '../connectors'; import { getAddress } from '@ethersproject/address'; +export const TOKEN_BLACKLIST = [ + '0x495c7f3a713870f68f8b418b355c085dfdc412c3', + '0xc3761eb917cd790b30dad99f6cc5b4ff93c4f9ea', + '0xe31debd7abff90b06bca21010dd860d8701fd901', + '0xfc989fbb6b3024de5ca0144dc23c18a063942ac1', + '0xf4eda77f0b455a12f3eb44f8653835f377e36b76', +]; + +export const PAIR_BLACKLIST = [ + '0xb6a741f37d6e455ebcc9f17e2c16d0586c3f57a5', + '0x97cb8cbe91227ba87fc21aaf52c4212d245da3f8', +]; + export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { INJECTED: { connector: injected, diff --git a/src/pages/AnalyticsPage/AnalyticsOverview.tsx b/src/pages/AnalyticsPage/AnalyticsOverview.tsx index d754a8bc2..6f66a44e5 100644 --- a/src/pages/AnalyticsPage/AnalyticsOverview.tsx +++ b/src/pages/AnalyticsPage/AnalyticsOverview.tsx @@ -97,6 +97,9 @@ const AnalyticsOverview: React.FC = ({ if (topTokensData) { updateTopTokens(topTokensData); } + }; + const fetchTopPairs = async () => { + const [newPrice] = await getEthPrice(); const pairs = await getTopPairs(8); const formattedPairs = pairs ? pairs.map((pair: any) => { @@ -109,7 +112,13 @@ const AnalyticsOverview: React.FC = ({ } }; fetchChartData(); - fetchTopTokens(); + if (!topTokens || topTokens.length < 8) { + fetchTopTokens(); + } + if (!topPairs || topPairs.length < 8) { + fetchTopPairs(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [startTime, updateGlobalChartData, updateTopTokens, updateTopPairs]); const liquidityDates = useMemo(() => { diff --git a/src/pages/AnalyticsPage/AnalyticsPage.tsx b/src/pages/AnalyticsPage/AnalyticsPage.tsx index c143b553a..5e45cb641 100755 --- a/src/pages/AnalyticsPage/AnalyticsPage.tsx +++ b/src/pages/AnalyticsPage/AnalyticsPage.tsx @@ -1,13 +1,20 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef, useMemo } from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { Box, Typography } from '@material-ui/core'; import cx from 'classnames'; import { useAnalyticToken } from 'state/application/hooks'; import { ReactComponent as SearchIcon } from 'assets/images/SearchIcon.svg'; +import { client } from 'apollo/client'; +import { TOKEN_SEARCH, PAIR_SEARCH } from 'apollo/queries'; +import { getAllTokensOnUniswap, getAllPairsOnUniswap } from 'utils'; +import { TOKEN_BLACKLIST, PAIR_BLACKLIST } from 'constants/index'; import AnalyticsOverview from './AnalyticsOverview'; import AnalyticsTokens from './AnalyticsTokens'; import AnalyticsPairs from './AnalyticsPairs'; import AnalyticsTokenDetails from './AnalyticTokenDetails'; +import { CurrencyLogo, DoubleCurrencyLogo } from 'components'; +import { ChainId, Token } from '@uniswap/sdk'; +import { getAddress } from '@ethersproject/address'; const useStyles = makeStyles(({}) => ({ topTab: { @@ -45,18 +52,220 @@ const useStyles = makeStyles(({}) => ({ color: '#ebecf2', }, }, + searchContent: { + position: 'absolute', + width: '100%', + background: '#1b1d26', + borderRadius: 10, + padding: 12, + zIndex: 2, + height: 300, + overflowY: 'auto', + }, })); const AnalyticsPage: React.FC = () => { const classes = useStyles(); const [tabIndex, setTabIndex] = useState(0); const [searchVal, setSearchVal] = useState(''); + const [menuOpen, setMenuOpen] = useState(false); + const menuRef = useRef(null); + const wrapperRef = useRef(null); const { analyticToken, updateAnalyticToken } = useAnalyticToken(); + const [searchedTokens, setSearchedTokens] = useState([]); + const [searchedPairs, setSearchedPairs] = useState([]); + const [tokensShown, setTokensShown] = useState(3); + const [pairsShown, setPairsShown] = useState(3); + const [allTokens, setAllTokens] = useState([]); + const [allPairs, setAllPairs] = useState([]); + + const escapeRegExp = (str: string) => { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + }; + + const filteredTokens = useMemo(() => { + const tokens = allTokens.concat( + searchedTokens.filter((searchedToken) => { + let included = false; + allTokens.map((token) => { + if (token.id === searchedToken.id) { + included = true; + } + return true; + }); + return !included; + }), + ); + const filtered = tokens + ? tokens.filter((token) => { + if (TOKEN_BLACKLIST.includes(token.id)) { + return false; + } + const regexMatches = Object.keys(token).map((tokenEntryKey) => { + const isAddress = searchVal.slice(0, 2) === '0x'; + if (tokenEntryKey === 'id' && isAddress) { + return token[tokenEntryKey].match( + new RegExp(escapeRegExp(searchVal), 'i'), + ); + } + if (tokenEntryKey === 'symbol' && !isAddress) { + return token[tokenEntryKey].match( + new RegExp(escapeRegExp(searchVal), 'i'), + ); + } + if (tokenEntryKey === 'name' && !isAddress) { + return token[tokenEntryKey].match( + new RegExp(escapeRegExp(searchVal), 'i'), + ); + } + return false; + }); + return regexMatches.some((m) => m); + }) + : []; + return filtered; + }, [allTokens, searchedTokens, searchVal]); + + const filteredPairs = useMemo(() => { + const pairs = allPairs.concat( + searchedPairs.filter((searchedPair) => { + let included = false; + allPairs.map((pair) => { + if (pair.id === searchedPair.id) { + included = true; + } + return true; + }); + return !included; + }), + ); + const filtered = pairs + ? pairs.filter((pair) => { + if (PAIR_BLACKLIST.includes(pair.id)) { + return false; + } + if (searchVal && searchVal.includes(' ')) { + const pairA = searchVal.split(' ')[0]?.toUpperCase(); + const pairB = searchVal.split(' ')[1]?.toUpperCase(); + return ( + (pair.token0.symbol.includes(pairA) || + pair.token0.symbol.includes(pairB)) && + (pair.token1.symbol.includes(pairA) || + pair.token1.symbol.includes(pairB)) + ); + } + if (searchVal && searchVal.includes('-')) { + const pairA = searchVal.split('-')[0]?.toUpperCase(); + const pairB = searchVal.split('-')[1]?.toUpperCase(); + return ( + (pair.token0.symbol.includes(pairA) || + pair.token0.symbol.includes(pairB)) && + (pair.token1.symbol.includes(pairA) || + pair.token1.symbol.includes(pairB)) + ); + } + const regexMatches = Object.keys(pair).map((field) => { + const isAddress = searchVal.slice(0, 2) === '0x'; + if (field === 'id' && isAddress) { + return pair[field].match( + new RegExp(escapeRegExp(searchVal), 'i'), + ); + } + if (field === 'token0') { + return ( + pair[field].symbol.match( + new RegExp(escapeRegExp(searchVal), 'i'), + ) || + pair[field].name.match(new RegExp(escapeRegExp(searchVal), 'i')) + ); + } + if (field === 'token1') { + return ( + pair[field].symbol.match( + new RegExp(escapeRegExp(searchVal), 'i'), + ) || + pair[field].name.match(new RegExp(escapeRegExp(searchVal), 'i')) + ); + } + return false; + }); + return regexMatches.some((m) => m); + }) + : []; + return filtered; + }, [allPairs, searchedPairs, searchVal]); + + useEffect(() => { + async function fetchAllData() { + const tokens = await getAllTokensOnUniswap(); + const pairs = await getAllPairsOnUniswap(); + if (tokens) { + setAllTokens(tokens); + } + if (pairs) { + setAllPairs(pairs); + } + } + fetchAllData(); + }, []); useEffect(() => { updateAnalyticToken(null); }, [updateAnalyticToken]); + useEffect(() => { + async function fetchData() { + try { + if (searchVal.length > 0) { + const tokens = await client.query({ + query: TOKEN_SEARCH, + variables: { + value: searchVal ? searchVal.toUpperCase() : '', + id: searchVal, + }, + }); + + const pairs = await client.query({ + query: PAIR_SEARCH, + variables: { + tokens: tokens.data.asSymbol?.map((t: any) => t.id), + id: searchVal, + }, + }); + + setSearchedPairs( + pairs.data.as0.concat(pairs.data.as1).concat(pairs.data.asAddress), + ); + const foundTokens = tokens.data.asSymbol + .concat(tokens.data.asAddress) + .concat(tokens.data.asName); + setSearchedTokens(foundTokens); + } + } catch (e) { + console.log(e); + } + } + fetchData(); + }, [searchVal]); + + const handleClick = (e: any) => { + if ( + !(menuRef.current && menuRef.current.contains(e.target)) && + !(wrapperRef.current && wrapperRef.current.contains(e.target)) + ) { + setPairsShown(3); + setTokensShown(3); + setMenuOpen(false); + } + }; + + useEffect(() => { + document.addEventListener('click', handleClick); + return () => { + document.removeEventListener('click', handleClick); + }; + }); + return ( {analyticToken ? ( @@ -68,12 +277,12 @@ const AnalyticsPage: React.FC = () => { - + { Pairs - - setSearchVal(evt.target.value)} - /> - - + + + setMenuOpen(true)} + onChange={(evt) => setSearchVal(evt.target.value)} + /> + + + + {menuOpen && ( +
+ Pairs + {filteredPairs.slice(0, pairsShown).map((val, ind) => { + const currency0 = new Token( + ChainId.MATIC, + getAddress(val.token0.id), + val.token0.decimals, + ); + const currency1 = new Token( + ChainId.MATIC, + getAddress(val.token1.id), + val.token1.decimals, + ); + return ( + + + + {val.token0.symbol} - {val.token1.symbol} Pair + + + ); + })} + setPairsShown(pairsShown + 5)} + > + Show More + + Tokens + {filteredTokens.slice(0, tokensShown).map((val, ind) => { + const currency = new Token( + ChainId.MATIC, + getAddress(val.id), + val.decimals, + ); + return ( + + + + {val.name} {val.symbol} + + + ); + })} + setTokensShown(tokensShown + 5)} + > + Show More + +
+ )}
{tabIndex === 0 && ( diff --git a/src/pages/AnalyticsPage/AnalyticsPairs.tsx b/src/pages/AnalyticsPage/AnalyticsPairs.tsx index 67e225661..a8f9f62e2 100644 --- a/src/pages/AnalyticsPage/AnalyticsPairs.tsx +++ b/src/pages/AnalyticsPage/AnalyticsPairs.tsx @@ -40,7 +40,10 @@ const AnalyticsPairs: React.FC = () => { updateTopPairs(pairData); } }; - fetchTopPairs(); + if (!topPairs || topPairs.length < 500) { + fetchTopPairs(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [updateTopPairs]); return ( diff --git a/src/utils/index.ts b/src/utils/index.ts index f73e6df33..49a9c26a3 100755 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -22,6 +22,8 @@ import { PAIRS_HISTORICAL_BULK, PRICES_BY_BLOCK, PAIRS_CURRENT, + ALL_PAIRS, + ALL_TOKENS, } from 'apollo/queries'; import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'; import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'; @@ -907,6 +909,56 @@ export async function getGlobalData( return data; } +export async function getAllPairsOnUniswap() { + try { + let allFound = false; + let pairs: any[] = []; + let skipCount = 0; + while (!allFound) { + const result = await client.query({ + query: ALL_PAIRS, + variables: { + skip: skipCount, + }, + fetchPolicy: 'cache-first', + }); + skipCount = skipCount + 10; + pairs = pairs.concat(result?.data?.pairs); + if (result?.data?.pairs.length < 10 || pairs.length > 10) { + allFound = true; + } + } + return pairs; + } catch (e) { + console.log(e); + } +} + +export async function getAllTokensOnUniswap() { + try { + let allFound = false; + let skipCount = 0; + let tokens: any[] = []; + while (!allFound) { + const result = await client.query({ + query: ALL_TOKENS, + variables: { + skip: skipCount, + }, + fetchPolicy: 'cache-first', + }); + tokens = tokens.concat(result?.data?.tokens); + if (result?.data?.tokens?.length < 10 || tokens.length > 10) { + allFound = true; + } + skipCount = skipCount += 10; + } + return tokens; + } catch (e) { + console.log(e); + } +} + export const getChartData = async (oldestDateToFetch: number) => { let data: any[] = []; const weeklyData: any[] = [];