├── .yarnrc ├── .env ├── .env.production ├── .vscode ├── settings.json └── launch.json ├── src ├── react-app-env.d.ts ├── assets │ ├── eth.png │ ├── placeholder.png │ ├── logo.svg │ ├── unicorn.svg │ └── logo_white.svg ├── components │ ├── Emoji │ │ └── index.js │ ├── analytics │ │ └── GoogleAnalyticsReporter.jsx │ ├── Attribution │ │ └── index.js │ ├── Column │ │ └── index.js │ ├── Footer │ │ └── index.js │ ├── Checkbox │ │ └── index.js │ ├── Dashboard │ │ └── index.js │ ├── Row │ │ └── index.js │ ├── DoubleLogo │ │ └── index.js │ ├── Toggle │ │ └── index.tsx │ ├── LocalLoader │ │ └── index.js │ ├── HoverText │ │ └── index.tsx │ ├── Copy │ │ └── index.js │ ├── Link │ │ └── index.js │ ├── FormattedName │ │ └── index.js │ ├── QuestionHelper │ │ └── index.tsx │ ├── CurrencySelect │ │ └── index.js │ ├── Popover │ │ └── index.tsx │ ├── Panel │ │ └── index.js │ ├── GlobalStats │ │ └── index.js │ ├── DropdownSelect │ │ └── index.js │ ├── TokenLogo │ │ └── index.js │ ├── UniPrice │ │ └── index.js │ ├── Title │ │ └── index.js │ ├── Select │ │ ├── popout.js │ │ ├── index.js │ │ └── styles.js │ ├── Warning │ │ └── index.js │ ├── ButtonStyled │ │ └── index.js │ ├── index.js │ ├── GlobalChart │ │ └── index.js │ ├── UserChart │ │ └── index.js │ ├── PinnedData │ │ └── index.js │ ├── AccountSearch │ │ └── index.js │ ├── PairReturnsChart │ │ └── index.js │ ├── LPList │ │ └── index.js │ ├── CandleChart │ │ └── index.js │ ├── SideNav │ │ └── index.js │ └── TradingviewChart │ │ └── index.js ├── apollo │ └── client.js ├── utils │ ├── data.ts │ └── tokenLists.ts ├── pages │ ├── AllTokensPage.js │ ├── AllPairsPage.js │ ├── AccountLookup.js │ └── GlobalPage.js ├── contexts │ ├── V1Data.js │ ├── LocalStorage.js │ └── Application.js ├── index.js ├── constants │ └── index.js ├── hooks │ └── index.ts ├── Theme │ └── index.js └── App.js ├── .prettierrc ├── public ├── favicon.ico ├── loading.gif ├── manifest.json └── index.html ├── webpack.config.js ├── tsconfig.strict.json ├── .gitignore ├── tsconfig.json ├── .eslintrc.json ├── README.md ├── .github └── workflows │ ├── lint.yml │ └── deploy.yaml ├── package.json └── cloudformation.template.json /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts true 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-5" -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-5" 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto27dev/pancake_fork/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto27dev/pancake_fork/HEAD/public/loading.gif -------------------------------------------------------------------------------- /src/assets/eth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto27dev/pancake_fork/HEAD/src/assets/eth.png -------------------------------------------------------------------------------- /src/assets/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crypto27dev/pancake_fork/HEAD/src/assets/placeholder.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | loaders: [ 4 | { 5 | test: /\.(png|jpg|gif)$/, 6 | loader: 'url?limit=25000', 7 | }, 8 | ], 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Emoji/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Emoji = (props) => ( 4 | 10 | {props.symbol} 11 | 12 | ) 13 | 14 | export default Emoji 15 | -------------------------------------------------------------------------------- /tsconfig.strict.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "alwaysStrict": true, 7 | "strictNullChecks": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "noUnusedLocals": true, 11 | "noFallthroughCasesInSwitch": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/analytics/GoogleAnalyticsReporter.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import ReactGA from 'react-ga' 3 | 4 | // fires a GA pageview every time the route changes 5 | export default function GoogleAnalyticsReporter({ location: { pathname, search } }) { 6 | useEffect(() => { 7 | ReactGA.pageview(`${pathname}${search}`) 8 | }, [pathname, search]) 9 | return null 10 | } 11 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Uniswap Info", 3 | "name": "View statistics for Uniswap exchanges.", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/components/Attribution/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Attribution = () => ( 4 |

5 | 6 | Github 7 | {' '} 8 | |{' '} 9 | 10 | Uniswap 11 | {' '} 12 | |{' '} 13 | 14 | GIF 15 | 16 |

17 | ) 18 | 19 | export default Attribution 20 | -------------------------------------------------------------------------------- /src/components/Column/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Column = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: flex-start; 7 | ` 8 | export const ColumnCenter = styled(Column)` 9 | width: 100%; 10 | align-items: center; 11 | ` 12 | 13 | export const AutoColumn = styled.div` 14 | display: grid; 15 | grid-auto-rows: auto; 16 | grid-row-gap: ${({ gap }) => (gap === 'sm' && '8px') || (gap === 'md' && '12px') || (gap === 'lg' && '24px') || gap}; 17 | justify-items: ${({ justify }) => justify && justify}; 18 | ` 19 | 20 | export default Column 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "downlevelIteration": true, 17 | "allowSyntheticDefaultImports": true, 18 | "types": ["react-spring", "jest"] 19 | }, 20 | "exclude": ["node_modules", "cypress"], 21 | "include": ["**/*.js", "**/*.ts", "**/*.tsx", "src/components/analytics/GoogleAnalyticsReporter.jsx"] 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Flex } from 'rebass' 3 | 4 | import Link from '../Link' 5 | 6 | const links = [ 7 | { url: 'https://uniswap.io', text: 'About' }, 8 | { url: 'https://docs.uniswap.io/', text: 'Docs' }, 9 | { url: 'https://github.com/Uniswap/uniswap-info', text: 'Code' }, 10 | ] 11 | 12 | const FooterLink = ({ children, ...rest }) => ( 13 | 14 | {children} 15 | 16 | ) 17 | 18 | const Footer = () => ( 19 | 20 | {links.map((link, index) => ( 21 | 22 | {link.text} 23 | 24 | ))} 25 | 26 | ) 27 | 28 | export default Footer 29 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | // Allows for the parsing of JSX 8 | "jsx": true 9 | } 10 | }, 11 | "ignorePatterns": ["node_modules/**/*"], 12 | "settings": { 13 | "react": { 14 | "version": "detect" 15 | } 16 | }, 17 | "extends": [ 18 | "plugin:react/recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "plugin:react-hooks/recommended", 21 | "prettier/@typescript-eslint", 22 | "plugin:prettier/recommended" 23 | ], 24 | "rules": { 25 | "@typescript-eslint/explicit-function-return-type": "off", 26 | "prettier/prettier": "error", 27 | "@typescript-eslint/no-explicit-any": "off" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uniswap Info (V1 + V2) 2 | 3 | [![Lint](https://github.com/Uniswap/uniswap-info/workflows/Lint/badge.svg)](https://github.com/Uniswap/uniswap-info/actions?query=workflow%3ALint) 4 | [![Deploy](https://github.com/Uniswap/uniswap-info/workflows/Deploy/badge.svg)](https://github.com/Uniswap/uniswap-info/actions?query=workflow%3ADeploy) 5 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 6 | 7 | Analytics site for the [Uniswap Protocol](https://uniswap.org). 8 | 9 | Includes support for Uniswap V1 and V2. For Uniswap V3 info see https://github.com/Uniswap/uniswap-v3-info 10 | 11 | ### To Start Development 12 | 13 | ###### Installing dependencies 14 | ```bash 15 | yarn 16 | ``` 17 | 18 | ###### Running locally 19 | ```bash 20 | yarn start 21 | ``` 22 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { TYPE } from '../../Theme' 4 | import { RowFixed } from '../Row' 5 | 6 | const StyleCheckbox = styled.input` 7 | background: ${({ theme }) => theme.bg2}; 8 | 9 | :before { 10 | background: #f35429; 11 | } 12 | 13 | :hover { 14 | cursor: pointer; 15 | } 16 | ` 17 | 18 | const ButtonText = styled(TYPE.main)` 19 | cursor: pointer; 20 | :hover { 21 | opacity: 0.6; 22 | } 23 | ` 24 | 25 | const CheckBox = ({ checked, setChecked, text }) => { 26 | return ( 27 | 28 | 29 | 30 | {text} 31 | 32 | 33 | ) 34 | } 35 | 36 | export default CheckBox 37 | -------------------------------------------------------------------------------- /src/components/Dashboard/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Box } from 'rebass' 3 | 4 | const Dashboard = styled(Box)` 5 | width: 100%; 6 | display: grid; 7 | grid-template-columns: 100%; 8 | grid-template-areas: 9 | 'volume' 10 | 'liquidity' 11 | 'shares' 12 | 'statistics' 13 | 'exchange' 14 | 'transactions'; 15 | 16 | @media screen and (min-width: 64em) { 17 | max-width: 1320px; 18 | grid-gap: 24px; 19 | width: 100%; 20 | grid-template-columns: 1fr 1fr 1fr; 21 | grid-template-areas: 22 | /* "statsHeader statsHeader statsHeader" */ 23 | 'fill fill fill' 24 | 'pairHeader pairHeader pairHeader' 25 | 'transactions2 transactions2 transactions2' 26 | 'listOptions listOptions listOptions' 27 | 'transactions transactions transactions'; 28 | } 29 | ` 30 | 31 | export default Dashboard 32 | -------------------------------------------------------------------------------- /src/components/Row/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Box } from 'rebass/styled-components' 3 | 4 | const Row = styled(Box)` 5 | width: 100%; 6 | display: flex; 7 | padding: 0; 8 | align-items: center; 9 | align-items: ${({ align }) => align && align}; 10 | padding: ${({ padding }) => padding}; 11 | border: ${({ border }) => border}; 12 | border-radius: ${({ borderRadius }) => borderRadius}; 13 | justify-content: ${({ justify }) => justify}; 14 | ` 15 | 16 | export const RowBetween = styled(Row)` 17 | justify-content: space-between; 18 | ` 19 | 20 | export const RowFlat = styled.div` 21 | display: flex; 22 | align-items: flex-end; 23 | ` 24 | 25 | export const AutoRow = styled(Row)` 26 | flex-wrap: ${({ wrap }) => wrap ?? 'nowrap'}; 27 | margin: -${({ gap }) => gap}; 28 | & > * { 29 | margin: ${({ gap }) => gap} !important; 30 | } 31 | ` 32 | 33 | export const RowFixed = styled(Row)` 34 | width: fit-content; 35 | ` 36 | 37 | export default Row 38 | -------------------------------------------------------------------------------- /src/components/DoubleLogo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import TokenLogo from '../TokenLogo' 4 | 5 | export default function DoubleTokenLogo({ a0, a1, size = 24, margin = false }) { 6 | const TokenWrapper = styled.div` 7 | position: relative; 8 | display: flex; 9 | flex-direction: row; 10 | margin-right: ${({ sizeraw, margin }) => margin && (sizeraw / 3 + 8).toString() + 'px'}; 11 | ` 12 | 13 | const HigherLogo = styled(TokenLogo)` 14 | z-index: 2; 15 | // background-color: white; 16 | border-radius: 50%; 17 | ` 18 | 19 | const CoveredLogo = styled(TokenLogo)` 20 | position: absolute; 21 | left: ${({ sizeraw }) => (sizeraw / 2).toString() + 'px'}; 22 | // background-color: white; 23 | border-radius: 50%; 24 | ` 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/components/Toggle/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Sun, Moon } from 'react-feather' 4 | 5 | const IconWrapper = styled.div<{ isActive?: boolean }>` 6 | opacity: ${({ isActive }) => (isActive ? 0.8 : 0.4)}; 7 | 8 | :hover { 9 | opacity: 1; 10 | } 11 | ` 12 | 13 | const StyledToggle = styled.div` 14 | display: flex; 15 | width: fit-content; 16 | cursor: pointer; 17 | text-decoration: none; 18 | margin-top: 1rem; 19 | color: white; 20 | 21 | :hover { 22 | text-decoration: none; 23 | } 24 | ` 25 | 26 | export interface ToggleProps { 27 | isActive: boolean 28 | toggle: () => void 29 | } 30 | 31 | export default function Toggle({ isActive, toggle }: ToggleProps) { 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | {' / '} 40 | 41 | 42 | 43 | 44 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/components/LocalLoader/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled, { css, keyframes } from 'styled-components' 3 | import { useDarkModeManager } from '../../contexts/LocalStorage' 4 | 5 | const pulse = keyframes` 6 | 0% { transform: scale(1); } 7 | 60% { transform: scale(1.1); } 8 | 100% { transform: scale(1); } 9 | ` 10 | 11 | const Wrapper = styled.div` 12 | pointer-events: none; 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | height: 100%; 17 | width: 100%; 18 | 19 | ${(props) => 20 | props.fill && !props.height 21 | ? css` 22 | height: 100vh; 23 | ` 24 | : css` 25 | height: 180px; 26 | `} 27 | ` 28 | 29 | const AnimatedImg = styled.div` 30 | animation: ${pulse} 800ms linear infinite; 31 | & > * { 32 | width: 72px; 33 | } 34 | ` 35 | 36 | const LocalLoader = ({ fill }) => { 37 | const [darkMode] = useDarkModeManager() 38 | 39 | return ( 40 | 41 | 42 | loading-icon 43 | 44 | 45 | ) 46 | } 47 | 48 | export default LocalLoader 49 | -------------------------------------------------------------------------------- /src/components/HoverText/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react' 2 | import styled from 'styled-components' 3 | import Popover, { PopoverProps } from '../Popover' 4 | 5 | const Wrapper = styled.span` 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | ` 10 | 11 | const TooltipContainer = styled.div` 12 | width: 228px; 13 | padding: 0.6rem 1rem; 14 | line-height: 150%; 15 | font-weight: 400; 16 | ` 17 | 18 | interface TooltipProps extends Omit { 19 | text: string 20 | } 21 | 22 | export function Tooltip({ text, ...rest }: TooltipProps) { 23 | return {text}} {...rest} /> 24 | } 25 | 26 | export default function HoverText({ text, children }: { text: string; children: any }) { 27 | const [show, setShow] = useState(false) 28 | const open = useCallback(() => setShow(true), [setShow]) 29 | const close = useCallback(() => setShow(false), [setShow]) 30 | 31 | return ( 32 | 33 | 34 | 35 | {children} 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/apollo/client.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient } from 'apollo-client' 2 | import { InMemoryCache } from 'apollo-cache-inmemory' 3 | import { HttpLink } from 'apollo-link-http' 4 | 5 | export const client = new ApolloClient({ 6 | link: new HttpLink({ 7 | uri: 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswapv2', 8 | }), 9 | cache: new InMemoryCache(), 10 | shouldBatch: true, 11 | }) 12 | 13 | export const healthClient = new ApolloClient({ 14 | link: new HttpLink({ 15 | uri: 'https://api.thegraph.com/index-node/graphql', 16 | }), 17 | cache: new InMemoryCache(), 18 | shouldBatch: true, 19 | }) 20 | 21 | export const v1Client = new ApolloClient({ 22 | link: new HttpLink({ 23 | uri: 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswap', 24 | }), 25 | cache: new InMemoryCache(), 26 | shouldBatch: true, 27 | }) 28 | 29 | export const stakingClient = new ApolloClient({ 30 | link: new HttpLink({ 31 | uri: 'https://api.thegraph.com/subgraphs/name/way2rach/talisman', 32 | }), 33 | cache: new InMemoryCache(), 34 | shouldBatch: true, 35 | }) 36 | 37 | export const blockClient = new ApolloClient({ 38 | link: new HttpLink({ 39 | uri: 'https://api.thegraph.com/subgraphs/name/blocklytics/ethereum-blocks', 40 | }), 41 | cache: new InMemoryCache(), 42 | }) 43 | -------------------------------------------------------------------------------- /src/components/Copy/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { useCopyClipboard } from '../../hooks' 4 | import { CheckCircle, Copy } from 'react-feather' 5 | import { StyledIcon } from '..' 6 | 7 | const CopyIcon = styled.div` 8 | color: #aeaeae; 9 | flex-shrink: 0; 10 | margin-right: 1rem; 11 | margin-left: 0.5rem; 12 | text-decoration: none; 13 | :hover, 14 | :active, 15 | :focus { 16 | text-decoration: none; 17 | opacity: 0.8; 18 | cursor: pointer; 19 | } 20 | ` 21 | const TransactionStatusText = styled.span` 22 | margin-left: 0.25rem; 23 | ${({ theme }) => theme.flexRowNoWrap}; 24 | align-items: center; 25 | color: black; 26 | ` 27 | 28 | export default function CopyHelper({ toCopy }) { 29 | const [isCopied, setCopied] = useCopyClipboard() 30 | 31 | return ( 32 | setCopied(toCopy)}> 33 | {isCopied ? ( 34 | 35 | 36 | 37 | 38 | 39 | ) : ( 40 | 41 | 42 | 43 | 44 | 45 | )} 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - v2 7 | pull_request_target: 8 | branches: 9 | - v2 10 | 11 | jobs: 12 | run-linters: 13 | name: Run linters 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Check out Git repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Set up node 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 12 24 | always-auth: true 25 | registry-url: https://registry.npmjs.org 26 | 27 | - name: Set output of cache 28 | id: yarn-cache 29 | run: echo "::set-output name=dir::$(yarn cache dir)" 30 | 31 | - name: Node dependency cache 32 | uses: actions/cache@v1 33 | with: 34 | path: ${{ steps.yarn-cache.outputs.dir }} 35 | key: yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | yarn- 38 | 39 | - name: Install dependencies 40 | run: yarn install --frozen-lockfile 41 | 42 | - name: Run linters 43 | uses: wearerequired/lint-action@77d70b9a07ecb93bc98dc46dc27d96c4f004d035 44 | with: 45 | github_token: ${{ secrets.github_token }} 46 | eslint: true 47 | eslint_extensions: js,jsx,ts,tsx,json 48 | auto_fix: true 49 | -------------------------------------------------------------------------------- /src/utils/data.ts: -------------------------------------------------------------------------------- 1 | interface BasicData { 2 | token0?: { 3 | id: string 4 | name: string 5 | symbol: string 6 | } 7 | token1?: { 8 | id: string 9 | name: string 10 | symbol: string 11 | } 12 | } 13 | 14 | // Override data return from graph - usually because proxy token has changed 15 | // names since entitiy was created in subgraph 16 | // keys are lowercase token addresses <-------- 17 | const TOKEN_OVERRIDES: { [address: string]: { name: string; symbol: string } } = { 18 | '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': { 19 | name: 'Ether (Wrapped)', 20 | symbol: 'ETH', 21 | }, 22 | '0x1416946162b1c2c871a73b07e932d2fb6c932069': { 23 | name: 'Energi', 24 | symbol: 'NRGE', 25 | }, 26 | } 27 | 28 | // override tokens with incorrect symbol or names 29 | export function updateNameData(data: BasicData): BasicData | undefined { 30 | if (data?.token0?.id && Object.keys(TOKEN_OVERRIDES).includes(data.token0.id)) { 31 | data.token0.name = TOKEN_OVERRIDES[data.token0.id].name 32 | data.token0.symbol = TOKEN_OVERRIDES[data.token0.id].symbol 33 | } 34 | 35 | if (data?.token1?.id && Object.keys(TOKEN_OVERRIDES).includes(data.token1.id)) { 36 | data.token1.name = TOKEN_OVERRIDES[data.token1.id].name 37 | data.token1.symbol = TOKEN_OVERRIDES[data.token1.id].symbol 38 | } 39 | 40 | return data 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Link/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link as RebassLink } from 'rebass' 3 | import { Link as RouterLink } from 'react-router-dom' 4 | import PropTypes from 'prop-types' 5 | import styled from 'styled-components' 6 | import { lighten, darken } from 'polished' 7 | 8 | const WrappedLink = ({ external, children, ...rest }) => ( 9 | 15 | {children} 16 | 17 | ) 18 | 19 | WrappedLink.propTypes = { 20 | external: PropTypes.bool, 21 | } 22 | 23 | const Link = styled(WrappedLink)` 24 | color: ${({ color, theme }) => (color ? color : theme.link)}; 25 | ` 26 | 27 | export default Link 28 | 29 | export const CustomLink = styled(RouterLink)` 30 | text-decoration: none; 31 | font-size: 14px; 32 | font-weight: 500; 33 | color: ${({ color, theme }) => (color ? color : theme.link)}; 34 | 35 | &:visited { 36 | color: ${({ color, theme }) => (color ? lighten(0.1, color) : lighten(0.1, theme.link))}; 37 | } 38 | 39 | &:hover { 40 | cursor: pointer; 41 | text-decoration: none; 42 | underline: none; 43 | color: ${({ color, theme }) => (color ? darken(0.1, color) : darken(0.1, theme.link))}; 44 | } 45 | ` 46 | 47 | export const BasicLink = styled(RouterLink)` 48 | text-decoration: none; 49 | color: inherit; 50 | &:hover { 51 | cursor: pointer; 52 | text-decoration: none; 53 | underline: none; 54 | } 55 | ` 56 | -------------------------------------------------------------------------------- /src/components/FormattedName/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from 'styled-components' 3 | import { Tooltip } from '../QuestionHelper' 4 | 5 | const TextWrapper = styled.div` 6 | position: relative; 7 | margin-left: ${({ margin }) => margin && '4px'}; 8 | color: ${({ theme, link }) => (link ? theme.blue : theme.text1)}; 9 | font-size: ${({ fontSize }) => fontSize ?? 'inherit'}; 10 | 11 | :hover { 12 | cursor: pointer; 13 | } 14 | 15 | @media screen and (max-width: 600px) { 16 | font-size: ${({ adjustSize }) => adjustSize && '12px'}; 17 | } 18 | ` 19 | 20 | const FormattedName = ({ text, maxCharacters, margin = false, adjustSize = false, fontSize, link, ...rest }) => { 21 | const [showHover, setShowHover] = useState(false) 22 | 23 | if (!text) { 24 | return '' 25 | } 26 | 27 | if (text.length > maxCharacters) { 28 | return ( 29 | 30 | setShowHover(true)} 32 | onMouseLeave={() => setShowHover(false)} 33 | margin={margin} 34 | adjustSize={adjustSize} 35 | link={link} 36 | fontSize={fontSize} 37 | {...rest} 38 | > 39 | {' ' + text.slice(0, maxCharacters - 1) + '...'} 40 | 41 | 42 | ) 43 | } 44 | 45 | return ( 46 | 47 | {text} 48 | 49 | ) 50 | } 51 | 52 | export default FormattedName 53 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | schedule: 5 | - cron: '0 12 * * 1-4' # every day 12:00 UTC Monday-Thursday 6 | 7 | # manual trigger 8 | workflow_dispatch: 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@master 16 | - name: Setup Node 17 | uses: actions/setup-node@v2-beta 18 | with: 19 | node-version: '12' 20 | - name: Install Dependencies 21 | run: yarn 22 | - name: Build 23 | run: yarn build 24 | - name: Deploy 25 | uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83 26 | with: 27 | args: "--exclude index.html --metadata-directive REPLACE --cache-control max-age=86400,public" 28 | env: 29 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 30 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 31 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 32 | AWS_REGION: 'us-east-1' 33 | SOURCE_DIR: 'build' 34 | - name: Deploy index.html 35 | uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83 36 | with: 37 | args: "--include index.html --metadata-directive REPLACE --cache-control max-age=0,must-revalidate" 38 | env: 39 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 40 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 41 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 42 | AWS_REGION: 'us-east-1' 43 | SOURCE_DIR: 'build' -------------------------------------------------------------------------------- /src/pages/AllTokensPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import 'feather-icons' 3 | 4 | import TopTokenList from '../components/TokenList' 5 | import { TYPE } from '../Theme' 6 | import Panel from '../components/Panel' 7 | import { useAllTokenData } from '../contexts/TokenData' 8 | import { PageWrapper, FullWrapper } from '../components' 9 | import { RowBetween } from '../components/Row' 10 | import Search from '../components/Search' 11 | import { useMedia } from 'react-use' 12 | // import CheckBox from '../components/Checkbox' 13 | // import QuestionHelper from '../components/QuestionHelper' 14 | 15 | function AllTokensPage() { 16 | const allTokens = useAllTokenData() 17 | 18 | useEffect(() => { 19 | window.scrollTo(0, 0) 20 | }, []) 21 | 22 | const below600 = useMedia('(max-width: 800px)') 23 | 24 | // const [useTracked, setUseTracked] = useState(true) 25 | 26 | return ( 27 | 28 | 29 | 30 | Top Tokens 31 | {!below600 && } 32 | 33 | {/* 34 | setUseTracked(!useTracked)} text={'Hide untracked tokens'} /> 35 | 36 | */} 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | export default AllTokensPage 46 | -------------------------------------------------------------------------------- /src/pages/AllPairsPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import 'feather-icons' 3 | 4 | import { TYPE } from '../Theme' 5 | import Panel from '../components/Panel' 6 | import { useAllPairData } from '../contexts/PairData' 7 | import PairList from '../components/PairList' 8 | import { PageWrapper, FullWrapper } from '../components' 9 | import { RowBetween, AutoRow } from '../components/Row' 10 | import Search from '../components/Search' 11 | import { useMedia } from 'react-use' 12 | import QuestionHelper from '../components/QuestionHelper' 13 | import CheckBox from '../components/Checkbox' 14 | 15 | function AllPairsPage() { 16 | const allPairs = useAllPairData() 17 | 18 | useEffect(() => { 19 | window.scrollTo(0, 0) 20 | }, []) 21 | 22 | const below800 = useMedia('(max-width: 800px)') 23 | 24 | const [useTracked, setUseTracked] = useState(true) 25 | 26 | return ( 27 | 28 | 29 | 30 | Top Pairs 31 | {!below800 && } 32 | 33 | 34 | setUseTracked(!useTracked)} text={'Hide untracked pairs'} /> 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | export default AllPairsPage 46 | -------------------------------------------------------------------------------- /src/components/QuestionHelper/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react' 2 | import { HelpCircle as Question } from 'react-feather' 3 | import styled from 'styled-components' 4 | import Popover, { PopoverProps } from '../Popover' 5 | 6 | const QuestionWrapper = styled.div` 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | padding: 0.2rem; 11 | border: none; 12 | background: none; 13 | outline: none; 14 | cursor: default; 15 | border-radius: 36px; 16 | background-color: ${({ theme }) => theme.bg2}; 17 | color: ${({ theme }) => theme.text2}; 18 | 19 | :hover, 20 | :focus { 21 | opacity: 0.7; 22 | } 23 | ` 24 | 25 | const TooltipContainer = styled.div` 26 | width: 228px; 27 | padding: 0.6rem 1rem; 28 | line-height: 150%; 29 | font-weight: 400; 30 | ` 31 | 32 | interface TooltipProps extends Omit { 33 | text: string 34 | } 35 | 36 | export function Tooltip({ text, ...rest }: TooltipProps) { 37 | return {text}} {...rest} /> 38 | } 39 | 40 | export default function QuestionHelper({ text, disabled }: { text: string; disabled?: boolean }) { 41 | const [show, setShow] = useState(false) 42 | 43 | const open = useCallback(() => setShow(true), [setShow]) 44 | const close = useCallback(() => setShow(false), [setShow]) 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/pages/AccountLookup.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import 'feather-icons' 3 | import { withRouter } from 'react-router-dom' 4 | import { TYPE } from '../Theme' 5 | import { PageWrapper, FullWrapper } from '../components' 6 | import Panel from '../components/Panel' 7 | import LPList from '../components/LPList' 8 | import styled from 'styled-components' 9 | import AccountSearch from '../components/AccountSearch' 10 | import { useTopLps } from '../contexts/GlobalData' 11 | import LocalLoader from '../components/LocalLoader' 12 | import { RowBetween } from '../components/Row' 13 | import { useMedia } from 'react-use' 14 | import Search from '../components/Search' 15 | 16 | const AccountWrapper = styled.div` 17 | @media screen and (max-width: 600px) { 18 | width: 100%; 19 | } 20 | ` 21 | 22 | function AccountLookup() { 23 | // scroll to top 24 | useEffect(() => { 25 | window.scrollTo(0, 0) 26 | }, []) 27 | 28 | const topLps = useTopLps() 29 | 30 | const below600 = useMedia('(max-width: 600px)') 31 | 32 | return ( 33 | 34 | 35 | 36 | Wallet analytics 37 | {!below600 && } 38 | 39 | 40 | 41 | 42 | 43 | Top Liquidity Positions 44 | 45 | {topLps && topLps.length > 0 ? : } 46 | 47 | 48 | ) 49 | } 50 | 51 | export default withRouter(AccountLookup) 52 | -------------------------------------------------------------------------------- /src/components/CurrencySelect/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { useCurrentCurrency } from '../../contexts/Application' 5 | 6 | import Row from '../Row' 7 | import { ChevronDown as Arrow } from 'react-feather' 8 | 9 | const Select = styled.div` 10 | position: relative; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: center; 15 | 16 | width: fit-content; 17 | height: 38px; 18 | border-radius: 20px; 19 | font-weight: 500; 20 | font-size: 1rem; 21 | color: ${({ theme }) => theme.textColor}; 22 | 23 | :hover { 24 | cursor: pointer; 25 | } 26 | 27 | @media screen and (max-width: 40em) { 28 | display: none; 29 | } 30 | ` 31 | 32 | const ArrowStyled = styled(Arrow)` 33 | height: 20px; 34 | width: 20px; 35 | margin-left: 6px; 36 | ` 37 | 38 | const Option = styled(Row)` 39 | position: absolute; 40 | top: 40px; 41 | ` 42 | 43 | const CurrencySelect = () => { 44 | const [showDropdown, toggleDropdown] = useState(false) 45 | const [currency, toggleCurrency] = useCurrentCurrency() 46 | 47 | const getOther = () => { 48 | if (currency === 'USD') { 49 | return 'ETH' 50 | } else { 51 | return 'USD' 52 | } 53 | } 54 | 55 | return ( 56 | <> 57 | 72 | 73 | ) 74 | } 75 | 76 | export default CurrencySelect 77 | -------------------------------------------------------------------------------- /src/contexts/V1Data.js: -------------------------------------------------------------------------------- 1 | import { v1Client } from '../apollo/client' 2 | import dayjs from 'dayjs' 3 | import utc from 'dayjs/plugin/utc' 4 | import { getPercentChange, get2DayPercentChange } from '../utils' 5 | import { V1_DATA_QUERY } from '../apollo/queries' 6 | import weekOfYear from 'dayjs/plugin/weekOfYear' 7 | 8 | dayjs.extend(utc) 9 | dayjs.extend(weekOfYear) 10 | 11 | export async function getV1Data() { 12 | dayjs.extend(utc) 13 | 14 | const utcCurrentTime = dayjs() 15 | const utcOneDayBack = utcCurrentTime.subtract(1, 'day').unix() 16 | const utcTwoDaysBack = utcCurrentTime.subtract(2, 'day').unix() 17 | 18 | try { 19 | // get the current data 20 | let result = await v1Client.query({ 21 | query: V1_DATA_QUERY, 22 | variables: { 23 | date: utcOneDayBack, 24 | date2: utcTwoDaysBack, 25 | }, 26 | fetchPolicy: 'cache-first', 27 | }) 28 | 29 | let data = result.data.current 30 | let oneDayData = result.data.oneDay[0] 31 | let twoDayData = result.data.twoDay[0] 32 | 33 | let [volumeChangeUSD, volumePercentChangeUSD] = get2DayPercentChange( 34 | data.totalVolumeUSD, 35 | oneDayData.totalVolumeUSD, 36 | twoDayData.totalVolumeUSD 37 | ) 38 | 39 | let [txCountChange, txCountPercentChange] = get2DayPercentChange( 40 | data.txCount, 41 | oneDayData.txCount, 42 | twoDayData.txCount 43 | ) 44 | 45 | // regular percent changes 46 | let liquidityPercentChangeUSD = getPercentChange(data.liquidityUsd, oneDayData.liquidityUsd) 47 | 48 | data.liquidityPercentChangeUSD = liquidityPercentChangeUSD 49 | data.volumePercentChangeUSD = volumePercentChangeUSD 50 | data.txCount = txCountChange 51 | data.txCountPercentChange = txCountPercentChange 52 | data.dailyVolumeUSD = volumeChangeUSD 53 | 54 | return data 55 | } catch (err) { 56 | console.log('error: ', err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | Uniswap Info 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/utils/tokenLists.ts: -------------------------------------------------------------------------------- 1 | import { TokenList } from '@uniswap/token-lists' 2 | import schema from '@uniswap/token-lists/src/tokenlist.schema.json' 3 | import Ajv from 'ajv' 4 | 5 | /** 6 | * Given a URI that may be ipfs, ipns, http, or https protocol, return the fetch-able http(s) URLs for the same content 7 | * @param uri to convert to fetch-able http url 8 | */ 9 | function uriToHttp(uri: string): string[] { 10 | const protocol = uri.split(':')[0].toLowerCase() 11 | switch (protocol) { 12 | case 'https': 13 | return [uri] 14 | case 'http': 15 | return ['https' + uri.substr(4), uri] 16 | case 'ipfs': 17 | const hash = uri.match(/^ipfs:(\/\/)?(.*)$/i)?.[2] 18 | return [`https://cloudflare-ipfs.com/ipfs/${hash}/`, `https://ipfs.io/ipfs/${hash}/`] 19 | case 'ipns': 20 | const name = uri.match(/^ipns:(\/\/)?(.*)$/i)?.[2] 21 | return [`https://cloudflare-ipfs.com/ipns/${name}/`, `https://ipfs.io/ipns/${name}/`] 22 | default: 23 | return [] 24 | } 25 | } 26 | 27 | const tokenListValidator = new Ajv({ allErrors: true }).compile(schema) 28 | 29 | /** 30 | * Contains the logic for resolving a list URL to a validated token list 31 | * @param listUrl list url 32 | */ 33 | export default async function getTokenList(listUrl: string): Promise { 34 | const urls = uriToHttp(listUrl) 35 | for (let i = 0; i < urls.length; i++) { 36 | const url = urls[i] 37 | const isLast = i === urls.length - 1 38 | let response 39 | try { 40 | response = await fetch(url) 41 | } catch (error) { 42 | console.debug('Failed to fetch list', listUrl, error) 43 | 44 | continue 45 | } 46 | 47 | if (!response.ok) { 48 | if (isLast) throw new Error(`Failed to download list ${listUrl}`) 49 | continue 50 | } 51 | 52 | const json = await response.json() 53 | if (!tokenListValidator(json)) { 54 | const validationErrors: string = 55 | tokenListValidator.errors?.reduce((memo, error) => { 56 | const add = `${error.dataPath} ${error.message ?? ''}` 57 | return memo.length > 0 ? `${memo}; ${add}` : `${add}` 58 | }, '') ?? 'unknown error' 59 | throw new Error(`Token list failed validation: ${validationErrors}`) 60 | } 61 | return json 62 | } 63 | // throw new Error('Unrecognized list URL protocol.') 64 | } 65 | -------------------------------------------------------------------------------- /src/components/Popover/index.tsx: -------------------------------------------------------------------------------- 1 | import { Placement } from '@popperjs/core' 2 | import { transparentize } from 'polished' 3 | import React, { useState } from 'react' 4 | import { usePopper } from 'react-popper' 5 | import styled from 'styled-components' 6 | import Portal from '@reach/portal' 7 | import useInterval from '../../hooks' 8 | 9 | const PopoverContainer = styled.div<{ show: boolean }>` 10 | z-index: 9999; 11 | 12 | visibility: ${(props) => (props.show ? 'visible' : 'hidden')}; 13 | opacity: ${(props) => (props.show ? 1 : 0)}; 14 | transition: visibility 150ms linear, opacity 150ms linear; 15 | 16 | background: ${({ theme }) => theme.bg2}; 17 | border: 1px solid ${({ theme }) => theme.bg3}; 18 | box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)}; 19 | color: ${({ theme }) => theme.text2}; 20 | border-radius: 8px; 21 | ` 22 | 23 | const ReferenceElement = styled.div` 24 | display: inline-block; 25 | ` 26 | 27 | export interface PopoverProps { 28 | content: React.ReactNode 29 | show: boolean 30 | children: React.ReactNode 31 | placement?: Placement 32 | } 33 | 34 | export default function Popover({ content, show, children, placement = 'auto' }: PopoverProps) { 35 | const [referenceElement, setReferenceElement] = useState(null) 36 | const [popperElement, setPopperElement] = useState(null) 37 | const [arrowElement] = useState(null) 38 | const { styles, update, attributes } = usePopper(referenceElement, popperElement, { 39 | placement, 40 | strategy: 'fixed', 41 | modifiers: [ 42 | { name: 'offset', options: { offset: [8, 8] } }, 43 | { name: 'arrow', options: { element: arrowElement } }, 44 | ], 45 | }) 46 | 47 | useInterval(update, show ? 100 : null) 48 | 49 | return ( 50 | <> 51 | {children} 52 | 53 | 54 | {content} 55 | {/* */} 61 | 62 | 63 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import ReactGA from 'react-ga' 4 | import { isMobile } from 'react-device-detect' 5 | import ThemeProvider, { GlobalStyle } from './Theme' 6 | import LocalStorageContextProvider, { Updater as LocalStorageContextUpdater } from './contexts/LocalStorage' 7 | import TokenDataContextProvider, { Updater as TokenDataContextUpdater } from './contexts/TokenData' 8 | import GlobalDataContextProvider from './contexts/GlobalData' 9 | import PairDataContextProvider, { Updater as PairDataContextUpdater } from './contexts/PairData' 10 | import ApplicationContextProvider from './contexts/Application' 11 | import UserContextProvider from './contexts/User' 12 | import App from './App' 13 | 14 | // initialize GA 15 | const GOOGLE_ANALYTICS_ID = process.env.REACT_APP_GOOGLE_ANALYTICS_ID 16 | 17 | if (typeof GOOGLE_ANALYTICS_ID === 'string') { 18 | ReactGA.initialize(GOOGLE_ANALYTICS_ID, { 19 | gaOptions: { 20 | storage: 'none', 21 | storeGac: false, 22 | }, 23 | }) 24 | ReactGA.set({ 25 | anonymizeIp: true, 26 | customBrowserType: !isMobile 27 | ? 'desktop' 28 | : 'web3' in window || 'ethereum' in window 29 | ? 'mobileWeb3' 30 | : 'mobileRegular', 31 | }) 32 | } else { 33 | ReactGA.initialize('test', { testMode: true, debug: true }) 34 | } 35 | 36 | function ContextProviders({ children }) { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | {children} 44 | 45 | 46 | 47 | 48 | 49 | ) 50 | } 51 | 52 | function Updaters() { 53 | return ( 54 | <> 55 | 56 | 57 | 58 | 59 | ) 60 | } 61 | 62 | ReactDOM.render( 63 | 64 | 65 | 66 | <> 67 | 68 | 69 | 70 | 71 | , 72 | document.getElementById('root') 73 | ) 74 | -------------------------------------------------------------------------------- /src/components/Panel/index.js: -------------------------------------------------------------------------------- 1 | import { Box as RebassBox } from 'rebass' 2 | import styled, { css } from 'styled-components' 3 | 4 | const panelPseudo = css` 5 | :after { 6 | content: ''; 7 | position: absolute; 8 | left: 0; 9 | right: 0; 10 | height: 10px; 11 | } 12 | 13 | @media only screen and (min-width: 40em) { 14 | :after { 15 | content: unset; 16 | } 17 | } 18 | ` 19 | 20 | const Panel = styled(RebassBox)` 21 | position: relative; 22 | background-color: ${({ theme }) => theme.advancedBG}; 23 | padding: 1.25rem; 24 | width: 100%; 25 | height: 100%; 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: flex-start; 29 | border-radius: 8px; 30 | border: 1px solid ${({ theme }) => theme.bg3}; 31 | box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.05); /* box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.01), 0px 16px 24px rgba(0, 0, 0, 0.01), 0px 24px 32px rgba(0, 0, 0, 0.01); */ 32 | :hover { 33 | cursor: ${({ hover }) => hover && 'pointer'}; 34 | border: ${({ hover, theme }) => hover && '1px solid' + theme.bg5}; 35 | } 36 | 37 | ${(props) => props.background && `background-color: ${props.theme.advancedBG};`} 38 | 39 | ${(props) => (props.area ? `grid-area: ${props.area};` : null)} 40 | 41 | ${(props) => 42 | props.grouped && 43 | css` 44 | @media only screen and (min-width: 40em) { 45 | &:first-of-type { 46 | border-radius: 20px 20px 0 0; 47 | } 48 | &:last-of-type { 49 | border-radius: 0 0 20px 20px; 50 | } 51 | } 52 | `} 53 | 54 | ${(props) => 55 | props.rounded && 56 | css` 57 | border-radius: 8px; 58 | @media only screen and (min-width: 40em) { 59 | border-radius: 10px; 60 | } 61 | `}; 62 | 63 | ${(props) => !props.last && panelPseudo} 64 | ` 65 | 66 | export default Panel 67 | 68 | // const Panel = styled.div` 69 | // width: 100%; 70 | // height: 100%; 71 | // display: flex; 72 | // flex-direction: column; 73 | // justify-content: flex-start; 74 | // border-radius: 12px; 75 | // background-color: ${({ theme }) => theme.advancedBG}; 76 | // padding: 1.25rem; 77 | // box-sizing: border-box; 78 | // box-shadow: 0 1.1px 2.8px -9px rgba(0, 0, 0, 0.008), 0 2.7px 6.7px -9px rgba(0, 0, 0, 0.012), 79 | // 0 5px 12.6px -9px rgba(0, 0, 0, 0.015), 0 8.9px 22.6px -9px rgba(0, 0, 0, 0.018), 80 | // 0 16.7px 42.2px -9px rgba(0, 0, 0, 0.022), 0 40px 101px -9px rgba(0, 0, 0, 0.03); 81 | // ` 82 | -------------------------------------------------------------------------------- /src/components/GlobalStats/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from 'styled-components' 3 | import { RowFixed, RowBetween } from '../Row' 4 | import { useMedia } from 'react-use' 5 | import { useGlobalData, useEthPrice } from '../../contexts/GlobalData' 6 | import { formattedNum, localNumber } from '../../utils' 7 | 8 | import UniPrice from '../UniPrice' 9 | import { TYPE } from '../../Theme' 10 | 11 | const Header = styled.div` 12 | width: 100%; 13 | position: sticky; 14 | top: 0; 15 | ` 16 | 17 | const Medium = styled.span` 18 | font-weight: 500; 19 | ` 20 | 21 | export default function GlobalStats() { 22 | const below1295 = useMedia('(max-width: 1295px)') 23 | const below1180 = useMedia('(max-width: 1180px)') 24 | const below1024 = useMedia('(max-width: 1024px)') 25 | const below400 = useMedia('(max-width: 400px)') 26 | const below816 = useMedia('(max-width: 816px)') 27 | 28 | const [showPriceCard, setShowPriceCard] = useState(false) 29 | 30 | const { oneDayVolumeUSD, oneDayTxns, pairCount } = useGlobalData() 31 | const [ethPrice] = useEthPrice() 32 | const formattedEthPrice = ethPrice ? formattedNum(ethPrice, true) : '-' 33 | const oneDayFees = oneDayVolumeUSD ? formattedNum(oneDayVolumeUSD * 0.003, true) : '' 34 | 35 | return ( 36 |
37 | 38 | 39 | {!below400 && ( 40 | { 43 | setShowPriceCard(true) 44 | }} 45 | onMouseLeave={() => { 46 | setShowPriceCard(false) 47 | }} 48 | style={{ position: 'relative' }} 49 | > 50 | ETH Price: {formattedEthPrice} 51 | {showPriceCard && } 52 | 53 | )} 54 | 55 | {!below1180 && ( 56 | 57 | Transactions (24H): {localNumber(oneDayTxns)} 58 | 59 | )} 60 | {!below1024 && ( 61 | 62 | Pairs: {localNumber(pairCount)} 63 | 64 | )} 65 | {!below1295 && ( 66 | 67 | Fees (24H): {oneDayFees}  68 | 69 | )} 70 | 71 | 72 |
73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /src/components/DropdownSelect/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from 'styled-components' 3 | 4 | import Row, { RowBetween } from '../Row' 5 | import { AutoColumn } from '../Column' 6 | import { ChevronDown as Arrow } from 'react-feather' 7 | import { TYPE } from '../../Theme' 8 | import { StyledIcon } from '..' 9 | 10 | const Wrapper = styled.div` 11 | z-index: 20; 12 | position: relative; 13 | background-color: ${({ theme }) => theme.panelColor}; 14 | border: 1px solid ${({ open, color }) => (open ? color : 'rgba(0, 0, 0, 0.15);')} 15 | width: 100px; 16 | padding: 4px 10px; 17 | padding-right: 6px; 18 | border-radius: 8px; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | 23 | :hover { 24 | cursor: pointer; 25 | } 26 | ` 27 | 28 | const Dropdown = styled.div` 29 | position: absolute; 30 | top: 34px; 31 | padding-top: 40px; 32 | width: calc(100% - 40px); 33 | background-color: ${({ theme }) => theme.bg1}; 34 | border: 1px solid rgba(0, 0, 0, 0.15); 35 | padding: 10px 10px; 36 | border-radius: 8px; 37 | width: calc(100% - 20px); 38 | font-weight: 500; 39 | font-size: 1rem; 40 | color: black; 41 | :hover { 42 | cursor: pointer; 43 | } 44 | ` 45 | 46 | const ArrowStyled = styled(Arrow)` 47 | height: 20px; 48 | width: 20px; 49 | margin-left: 6px; 50 | ` 51 | 52 | const DropdownSelect = ({ options, active, setActive, color }) => { 53 | const [showDropdown, toggleDropdown] = useState(false) 54 | 55 | return ( 56 | 57 | toggleDropdown(!showDropdown)} justify="center"> 58 | {active} 59 | 60 | 61 | 62 | 63 | {showDropdown && ( 64 | 65 | 66 | {Object.keys(options).map((key, index) => { 67 | let option = options[key] 68 | return ( 69 | option !== active && ( 70 | { 72 | toggleDropdown(!showDropdown) 73 | setActive(option) 74 | }} 75 | key={index} 76 | > 77 | {option} 78 | 79 | ) 80 | ) 81 | })} 82 | 83 | 84 | )} 85 | 86 | ) 87 | } 88 | 89 | export default DropdownSelect 90 | -------------------------------------------------------------------------------- /src/components/TokenLogo/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import styled from 'styled-components' 3 | import { isAddress } from '../../utils/index.js' 4 | import EthereumLogo from '../../assets/eth.png' 5 | 6 | const BAD_IMAGES = {} 7 | 8 | const Inline = styled.div` 9 | display: flex; 10 | align-items: center; 11 | align-self: center; 12 | ` 13 | 14 | const Image = styled.img` 15 | width: ${({ size }) => size}; 16 | height: ${({ size }) => size}; 17 | background-color: white; 18 | border-radius: 50%; 19 | box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); 20 | ` 21 | 22 | const StyledEthereumLogo = styled.div` 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | 27 | > img { 28 | width: ${({ size }) => size}; 29 | height: ${({ size }) => size}; 30 | } 31 | ` 32 | 33 | export default function TokenLogo({ address, header = false, size = '24px', ...rest }) { 34 | const [error, setError] = useState(false) 35 | 36 | useEffect(() => { 37 | setError(false) 38 | }, [address]) 39 | 40 | if (error || BAD_IMAGES[address]) { 41 | return ( 42 | 43 | 44 | 🤔 45 | 46 | 47 | ) 48 | } 49 | 50 | // hard coded fixes for trust wallet api issues 51 | if (address?.toLowerCase() === '0x5e74c9036fb86bd7ecdcb084a0673efc32ea31cb') { 52 | address = '0x42456d7084eacf4083f1140d3229471bba2949a8' 53 | } 54 | 55 | if (address?.toLowerCase() === '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f') { 56 | address = '0xc011a72400e58ecd99ee497cf89e3775d4bd732f' 57 | } 58 | 59 | if (address?.toLowerCase() === '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') { 60 | return ( 61 | 62 | 70 | 71 | ) 72 | } 73 | 74 | const path = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${isAddress( 75 | address 76 | )}/logo.png` 77 | 78 | return ( 79 | 80 | {''} { 86 | BAD_IMAGES[address] = true 87 | setError(true) 88 | event.preventDefault() 89 | }} 90 | /> 91 | 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /src/components/UniPrice/index.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import styled from 'styled-components' 3 | import Panel from '../Panel' 4 | import { AutoColumn } from '../Column' 5 | import { RowFixed } from '../Row' 6 | import { TYPE } from '../../Theme' 7 | import { usePairData } from '../../contexts/PairData' 8 | import { formattedNum } from '../../utils' 9 | 10 | const PriceCard = styled(Panel)` 11 | position: absolute; 12 | right: -220px; 13 | width: 220px; 14 | top: -20px; 15 | z-index: 9999; 16 | height: fit-content; 17 | background-color: ${({ theme }) => theme.bg1}; 18 | ` 19 | 20 | function formatPercent(rawPercent) { 21 | if (rawPercent < 0.01) { 22 | return '<1%' 23 | } else return parseFloat(rawPercent * 100).toFixed(0) + '%' 24 | } 25 | 26 | export default function UniPrice() { 27 | const daiPair = usePairData('0xa478c2975ab1ea89e8196811f51a7b7ade33eb11') 28 | const usdcPair = usePairData('0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc') 29 | const usdtPair = usePairData('0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852') 30 | 31 | const totalLiquidity = useMemo(() => { 32 | return daiPair && usdcPair && usdtPair 33 | ? daiPair.trackedReserveUSD + usdcPair.trackedReserveUSD + usdtPair.trackedReserveUSD 34 | : 0 35 | }, [daiPair, usdcPair, usdtPair]) 36 | 37 | const daiPerEth = daiPair ? parseFloat(daiPair.token0Price).toFixed(2) : '-' 38 | const usdcPerEth = usdcPair ? parseFloat(usdcPair.token0Price).toFixed(2) : '-' 39 | const usdtPerEth = usdtPair ? parseFloat(usdtPair.token1Price).toFixed(2) : '-' 40 | 41 | return ( 42 | 43 | 44 | 45 | DAI/ETH: {formattedNum(daiPerEth, true)} 46 | 47 | {daiPair && totalLiquidity ? formatPercent(daiPair.trackedReserveUSD / totalLiquidity) : '-'} 48 | 49 | 50 | 51 | USDC/ETH: {formattedNum(usdcPerEth, true)} 52 | 53 | {usdcPair && totalLiquidity ? formatPercent(usdcPair.trackedReserveUSD / totalLiquidity) : '-'} 54 | 55 | 56 | 57 | USDT/ETH: {formattedNum(usdtPerEth, true)} 58 | 59 | {usdtPair && totalLiquidity ? formatPercent(usdtPair.trackedReserveUSD / totalLiquidity) : '-'} 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const FACTORY_ADDRESS = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f' 2 | 3 | export const BUNDLE_ID = '1' 4 | 5 | export const timeframeOptions = { 6 | WEEK: '1 week', 7 | MONTH: '1 month', 8 | // THREE_MONTHS: '3 months', 9 | // YEAR: '1 year', 10 | HALF_YEAR: '6 months', 11 | ALL_TIME: 'All time', 12 | } 13 | 14 | // token list urls to fetch tokens from - use for warnings on tokens and pairs 15 | export const SUPPORTED_LIST_URLS__NO_ENS = [ 16 | 'https://gateway.ipfs.io/ipns/tokens.uniswap.org', 17 | 'https://www.coingecko.com/tokens_list/uniswap/defi_100/v_0_0_0.json', 18 | ] 19 | 20 | // hide from overview list 21 | export const TOKEN_BLACKLIST = [ 22 | '0x495c7f3a713870f68f8b418b355c085dfdc412c3', 23 | '0xc3761eb917cd790b30dad99f6cc5b4ff93c4f9ea', 24 | '0xe31debd7abff90b06bca21010dd860d8701fd901', 25 | '0xfc989fbb6b3024de5ca0144dc23c18a063942ac1', 26 | '0xf4eda77f0b455a12f3eb44f8653835f377e36b76', 27 | '0x93b2fff814fcaeffb01406e80b4ecd89ca6a021b', 28 | 29 | // rebass tokens 30 | '0x9ea3b5b4ec044b70375236a281986106457b20ef', 31 | '0x05934eba98486693aaec2d00b0e9ce918e37dc3f', 32 | '0x3d7e683fc9c86b4d653c9e47ca12517440fad14e', 33 | '0xfae9c647ad7d89e738aba720acf09af93dc535f7', 34 | '0x7296368fe9bcb25d3ecc19af13655b907818cc09', 35 | ] 36 | 37 | // pair blacklist 38 | export const PAIR_BLACKLIST = [ 39 | '0xb6a741f37d6e455ebcc9f17e2c16d0586c3f57a5', 40 | '0x97cb8cbe91227ba87fc21aaf52c4212d245da3f8', 41 | '0x1acba73121d5f63d8ea40bdc64edb594bd88ed09', 42 | '0x7d7e813082ef6c143277c71786e5be626ec77b20', 43 | ] 44 | 45 | // warnings to display if page contains info about blocked token 46 | export const BLOCKED_WARNINGS = { 47 | '0xf4eda77f0b455a12f3eb44f8653835f377e36b76': 48 | 'TikTok Inc. has asserted this token is violating its trademarks and therefore is not available.', 49 | } 50 | 51 | /** 52 | * For tokens that cause erros on fee calculations 53 | */ 54 | export const FEE_WARNING_TOKENS = ['0xd46ba6d942050d489dbd938a2c909a5d5039a161'] 55 | 56 | export const UNTRACKED_COPY = 'Derived USD values may be inaccurate without liquid stablecoin or ETH pairings.' 57 | 58 | // pairs that should be tracked but arent due to lag in subgraph 59 | export const TRACKED_OVERRIDES_PAIRS = [ 60 | '0x9928e4046d7c6513326ccea028cd3e7a91c7590a', 61 | '0x87da823b6fc8eb8575a235a824690fda94674c88', 62 | '0xcd7989894bc033581532d2cd88da5db0a4b12859', 63 | '0xe1573b9d29e2183b1af0e743dc2754979a40d237', 64 | '0x45804880de22913dafe09f4980848ece6ecbaf78', 65 | '0x709f7b10f22eb62b05913b59b92ddd372d4e2152', 66 | ] 67 | 68 | // tokens that should be tracked but arent due to lag in subgraph 69 | // all pairs that include token will be tracked 70 | export const TRACKED_OVERRIDES_TOKENS = ['0x956f47f50a910163d8bf957cf5846d573e7f87ca'] 71 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useEffect, useRef } from 'react' 2 | import { shade } from 'polished' 3 | import Vibrant from 'node-vibrant' 4 | import { hex } from 'wcag-contrast' 5 | import { isAddress } from '../utils' 6 | import copy from 'copy-to-clipboard' 7 | 8 | export function useColor(tokenAddress, token) { 9 | const [color, setColor] = useState('#2172E5') 10 | if (tokenAddress) { 11 | const path = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${isAddress( 12 | tokenAddress 13 | )}/logo.png` 14 | if (path) { 15 | Vibrant.from(path).getPalette((err, palette) => { 16 | if (palette && palette.Vibrant) { 17 | let detectedHex = palette.Vibrant.hex 18 | let AAscore = hex(detectedHex, '#FFF') 19 | while (AAscore < 3) { 20 | detectedHex = shade(0.005, detectedHex) 21 | AAscore = hex(detectedHex, '#FFF') 22 | } 23 | if (token === 'DAI') { 24 | setColor('#FAAB14') 25 | } else { 26 | setColor(detectedHex) 27 | } 28 | } 29 | }) 30 | } 31 | } 32 | return color 33 | } 34 | 35 | export function useCopyClipboard(timeout = 500) { 36 | const [isCopied, setIsCopied] = useState(false) 37 | 38 | const staticCopy = useCallback((text) => { 39 | const didCopy = copy(text) 40 | setIsCopied(didCopy) 41 | }, []) 42 | 43 | useEffect(() => { 44 | if (isCopied) { 45 | const hide = setTimeout(() => { 46 | setIsCopied(false) 47 | }, timeout) 48 | 49 | return () => { 50 | clearTimeout(hide) 51 | } 52 | } 53 | }, [isCopied, setIsCopied, timeout]) 54 | 55 | return [isCopied, staticCopy] 56 | } 57 | 58 | export const useOutsideClick = (ref, ref2, callback) => { 59 | const handleClick = (e) => { 60 | if (ref.current && ref.current && !ref2.current) { 61 | callback(true) 62 | } else if (ref.current && !ref.current.contains(e.target) && ref2.current && !ref2.current.contains(e.target)) { 63 | callback(true) 64 | } else { 65 | callback(false) 66 | } 67 | } 68 | useEffect(() => { 69 | document.addEventListener('click', handleClick) 70 | return () => { 71 | document.removeEventListener('click', handleClick) 72 | } 73 | }) 74 | } 75 | 76 | export default function useInterval(callback: () => void, delay: null | number) { 77 | const savedCallback = useRef<() => void>() 78 | 79 | // Remember the latest callback. 80 | useEffect(() => { 81 | savedCallback.current = callback 82 | }, [callback]) 83 | 84 | // Set up the interval. 85 | useEffect(() => { 86 | function tick() { 87 | const current = savedCallback.current 88 | current && current() 89 | } 90 | 91 | if (delay !== null) { 92 | tick() 93 | const id = setInterval(tick, delay) 94 | return () => clearInterval(id) 95 | } 96 | return 97 | }, [delay]) 98 | } 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uniswap/info", 3 | "private": true, 4 | "homepage": "https://info.uniswap.org", 5 | "devDependencies": { 6 | "@popperjs/core": "^2.4.2", 7 | "@reach/portal": "^0.10.3", 8 | "@reach/router": "^1.2.1", 9 | "@types/jest": "^26.0.0", 10 | "@types/node": "^14.0.13", 11 | "@types/react": "^16.9.36", 12 | "@types/react-dom": "^16.9.8", 13 | "@types/styled-components": "^5.1.0", 14 | "@typescript-eslint/eslint-plugin": "^4.4.0", 15 | "@typescript-eslint/parser": "^4.4.0", 16 | "@uniswap/token-lists": "^1.0.0-beta.15", 17 | "ajv": "^6.12.4", 18 | "animated-number-react": "^0.1.1", 19 | "apollo-cache-inmemory": "^1.6.3", 20 | "apollo-client": "^2.6.4", 21 | "apollo-link-http": "^1.5.16", 22 | "bignumber.js": "^9.0.0", 23 | "color-contrast-checker": "^1.5.0", 24 | "copy-to-clipboard": "^3.3.1", 25 | "dayjs": "^1.8.16", 26 | "decimal.js-light": "^2.5.0", 27 | "es-abstract": "^1.14.2", 28 | "eslint": "^6.8.0", 29 | "eslint-config-prettier": "^6.12.0", 30 | "eslint-plugin-prettier": "^3.1.4", 31 | "eslint-plugin-react": "^7.21.3", 32 | "eslint-plugin-react-hooks": "^4.1.2", 33 | "ethers": "^4.0.39", 34 | "export-to-csv": "^0.2.1", 35 | "feather-icons": "^4.24.1", 36 | "graphql": "^14.5.6", 37 | "graphql-tag": "^2.10.1", 38 | "jazzicon": "^1.5.0", 39 | "lightweight-charts": "^3.1.3", 40 | "node-vibrant": "^3.1.5", 41 | "numeral": "^2.0.6", 42 | "polished": "^3.4.4", 43 | "prettier": "^2.1.2", 44 | "react": "^16.9.0", 45 | "react-apollo": "^3.1.1", 46 | "react-device-detect": "^1.9.10", 47 | "react-dom": "^16.9.0", 48 | "react-feather": "^2.0.3", 49 | "react-ga": "^3.1.2", 50 | "react-iframe": "^1.8.0", 51 | "react-parallax": "^2.2.4", 52 | "react-popper": "^2.2.3", 53 | "react-resize-observer": "^1.1.1", 54 | "react-router-dom": "^5.2.0", 55 | "react-scripts": "^3.1.1", 56 | "react-scroll": "^1.8.0", 57 | "react-scroll-parallax": "^2.2.0", 58 | "react-select": "^3.0.7", 59 | "react-spring": "^8.0.27", 60 | "react-springy-parallax": "^1.3.0", 61 | "react-switch": "^5.0.1", 62 | "react-use": "^12.2.0", 63 | "rebass": "^4.0.7", 64 | "recharts": "^1.7.1", 65 | "recharts-fork": "^1.7.2", 66 | "styled-components": "^4.3.2", 67 | "toformat": "^2.0.0", 68 | "typeface-inter": "^3.10.0", 69 | "typescript": "^3.9.5", 70 | "unstated": "^2.1.1", 71 | "wcag-contrast": "^3.0.0" 72 | }, 73 | "scripts": { 74 | "start": "react-scripts start", 75 | "build": "react-scripts build", 76 | "test": "react-scripts test", 77 | "eject": "react-scripts eject" 78 | }, 79 | "browserslist": { 80 | "production": [ 81 | ">0.2%", 82 | "not dead", 83 | "not op_mini all" 84 | ], 85 | "development": [ 86 | "last 1 chrome version", 87 | "last 1 firefox version", 88 | "last 1 safari version" 89 | ] 90 | }, 91 | "license": "GPL-3.0-or-later" 92 | } 93 | -------------------------------------------------------------------------------- /src/components/Title/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useHistory } from 'react-router-dom' 3 | import styled from 'styled-components' 4 | 5 | import { Flex } from 'rebass' 6 | import Link from '../Link' 7 | import { RowFixed } from '../Row' 8 | import Logo from '../../assets/logo_white.svg' 9 | import Wordmark from '../../assets/wordmark_white.svg' 10 | 11 | import { BasicLink } from '../Link' 12 | import { useMedia } from 'react-use' 13 | 14 | const TitleWrapper = styled.div` 15 | text-decoration: none; 16 | z-index: 10; 17 | width: 100%; 18 | &:hover { 19 | cursor: pointer; 20 | } 21 | ` 22 | 23 | const UniIcon = styled(Link)` 24 | transition: transform 0.3s ease; 25 | :hover { 26 | transform: rotate(-5deg); 27 | } 28 | ` 29 | 30 | const Option = styled.div` 31 | font-weight: 500; 32 | font-size: 14px; 33 | opacity: ${({ activeText }) => (activeText ? 1 : 0.6)}; 34 | color: ${({ theme }) => theme.white}; 35 | display: flex; 36 | margin-left: 12px; 37 | :hover { 38 | opacity: 1; 39 | } 40 | ` 41 | 42 | export default function Title() { 43 | const history = useHistory() 44 | const below1080 = useMedia('(max-width: 1080px)') 45 | 46 | return ( 47 | history.push('/')}> 48 | 49 | 50 | history.push('/')}> 51 | logo 52 | 53 | {!below1080 && ( 54 | logo 55 | )} 56 | 57 | {below1080 && ( 58 | 59 | 60 | 61 | 62 | 63 | 72 | 73 | 74 | 83 | 84 | 85 | 86 | 95 | 96 | 97 | )} 98 | 99 | 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/components/Select/popout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Button } from 'rebass' 3 | import styled from 'styled-components' 4 | 5 | import Select from 'react-select' 6 | 7 | const selectStyles = { 8 | control: (styles) => ({ 9 | ...styles, 10 | padding: '1rem', 11 | border: 'none', 12 | backgroundColor: 'transparent', 13 | borderBottom: '1px solid #e1e1e1', 14 | boxShadow: 'none', 15 | borderRadius: 0, 16 | ':hover': { 17 | borderColor: '#e1e1e1', 18 | }, 19 | }), 20 | valueContainer: (styles) => ({ 21 | ...styles, 22 | padding: 0, 23 | }), 24 | menu: () => null, 25 | } 26 | 27 | export default class Popout extends Component { 28 | state = { isOpen: false, value: undefined } 29 | toggleOpen = () => { 30 | this.setState((state) => ({ isOpen: !state.isOpen })) 31 | } 32 | 33 | onSelectChange = (value) => { 34 | this.toggleOpen() 35 | this.setState({ value }) 36 | } 37 | 38 | render() { 39 | const { isOpen, value } = this.state 40 | return ( 41 | 56 | {value ? value.label : 'Select...'} 57 | 58 | } 59 | > 60 | { 98 | setCapEth(!capEth) 99 | }} 100 | /> 101 | Hide Low Liquidity 102 | 103 | {children} 104 | 105 | ) 106 | }, 107 | }} 108 | /> 109 | ) : ( 110 | 118 | ) 119 | } 120 | 121 | Select.propTypes = { 122 | options: PropTypes.array.isRequired, 123 | onChange: PropTypes.func, 124 | } 125 | 126 | export default Select 127 | 128 | export { Popout } 129 | -------------------------------------------------------------------------------- /src/components/Warning/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import 'feather-icons' 3 | import styled from 'styled-components' 4 | import { Text } from 'rebass' 5 | import { AlertTriangle } from 'react-feather' 6 | import { RowBetween, RowFixed } from '../Row' 7 | import { ButtonDark } from '../ButtonStyled' 8 | import { AutoColumn } from '../Column' 9 | import { Hover } from '..' 10 | import Link from '../Link' 11 | import { useMedia } from 'react-use' 12 | 13 | const WarningWrapper = styled.div` 14 | border-radius: 20px; 15 | border: 1px solid #f82d3a; 16 | background: rgba(248, 45, 58, 0.05); 17 | padding: 1rem; 18 | color: #f82d3a; 19 | display: ${({ show }) => !show && 'none'}; 20 | margin: 0 2rem 2rem 2rem; 21 | position: relative; 22 | 23 | @media screen and (max-width: 800px) { 24 | width: 80% !important; 25 | margin-left: 5%; 26 | } 27 | ` 28 | 29 | const StyledWarningIcon = styled(AlertTriangle)` 30 | min-height: 20px; 31 | min-width: 20px; 32 | stroke: red; 33 | ` 34 | 35 | export default function Warning({ type, show, setShow, address }) { 36 | const below800 = useMedia('(max-width: 800px)') 37 | 38 | const textContent = below800 ? ( 39 |
40 | 41 | Anyone can create and name any ERC20 token on Ethereum, including creating fake versions of existing tokens and 42 | tokens that claim to represent projects that do not have a token. 43 | 44 | 45 | Similar to Etherscan, this site automatically tracks analytics for all ERC20 tokens independent of token 46 | integrity. Please do your own research before interacting with any ERC20 token. 47 | 48 |
49 | ) : ( 50 | 51 | Anyone can create and name any ERC20 token on Ethereum, including creating fake versions of existing tokens and 52 | tokens that claim to represent projects that do not have a token. Similar to Etherscan, this site automatically 53 | tracks analytics for all ERC20 tokens independent of token integrity. Please do your own research before 54 | interacting with any ERC20 token. 55 | 56 | ) 57 | 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | Token Safety Alert 65 | 66 | 67 | {textContent} 68 | {below800 ? ( 69 |
70 | 71 | 78 | View {type === 'token' ? 'token' : 'pair'} contract on Etherscan 79 | 80 | 81 | 82 |
83 | setShow(false)}> 84 | I understand 85 | 86 | 87 |
88 | ) : ( 89 | 90 | 91 | 98 | View {type === 'token' ? 'token' : 'pair'} contract on Etherscan 99 | 100 | 101 | setShow(false)}> 102 | I understand 103 | 104 | 105 | )} 106 | 107 | 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /src/components/ButtonStyled/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button as RebassButton } from 'rebass/styled-components' 3 | import styled from 'styled-components' 4 | import { Plus, ChevronDown, ChevronUp } from 'react-feather' 5 | import { darken, transparentize } from 'polished' 6 | import { RowBetween } from '../Row' 7 | import { StyledIcon } from '..' 8 | 9 | const Base = styled(RebassButton)` 10 | padding: 8px 12px; 11 | font-size: 0.825rem; 12 | font-weight: 600; 13 | border-radius: 12px; 14 | cursor: pointer; 15 | outline: none; 16 | border: 1px solid transparent; 17 | outline: none; 18 | border-bottom-right-radius: ${({ open }) => open && '0'}; 19 | border-bottom-left-radius: ${({ open }) => open && '0'}; 20 | ` 21 | 22 | const BaseCustom = styled(RebassButton)` 23 | padding: 16px 12px; 24 | font-size: 0.825rem; 25 | font-weight: 400; 26 | border-radius: 12px; 27 | cursor: pointer; 28 | outline: none; 29 | ` 30 | 31 | const Dull = styled(Base)` 32 | background-color: rgba(255, 255, 255, 0.15); 33 | border: 1px solid rgba(255, 255, 255, 0.15); 34 | color: black; 35 | height: 100%; 36 | font-weight: 400; 37 | &:hover, 38 | :focus { 39 | background-color: rgba(255, 255, 255, 0.25); 40 | border-color: rgba(255, 255, 255, 0.25); 41 | } 42 | &:focus { 43 | box-shadow: 0 0 0 1pt rgba(255, 255, 255, 0.25); 44 | } 45 | &:active { 46 | background-color: rgba(255, 255, 255, 0.25); 47 | border-color: rgba(255, 255, 255, 0.25); 48 | } 49 | ` 50 | 51 | export default function ButtonStyled({ children, ...rest }) { 52 | return {children} 53 | } 54 | 55 | const ContentWrapper = styled.div` 56 | display: flex; 57 | flex-direction: row; 58 | align-items: center; 59 | justify-content: space-between; 60 | ` 61 | 62 | export const ButtonLight = styled(Base)` 63 | background-color: ${({ color, theme }) => (color ? transparentize(0.9, color) : transparentize(0.9, theme.primary1))}; 64 | color: ${({ color, theme }) => (color ? darken(0.1, color) : theme.primary1)}; 65 | 66 | min-width: fit-content; 67 | border-radius: 12px; 68 | white-space: nowrap; 69 | 70 | a { 71 | color: ${({ color, theme }) => (color ? darken(0.1, color) : theme.primary1)}; 72 | } 73 | 74 | :hover { 75 | background-color: ${({ color, theme }) => 76 | color ? transparentize(0.8, color) : transparentize(0.8, theme.primary1)}; 77 | } 78 | ` 79 | 80 | export function ButtonDropdown({ disabled = false, children, open, ...rest }) { 81 | return ( 82 | 83 | 84 |
{children}
85 | {open ? ( 86 | 87 | 88 | 89 | ) : ( 90 | 91 | 92 | 93 | )} 94 |
95 |
96 | ) 97 | } 98 | 99 | export const ButtonDark = styled(Base)` 100 | background-color: ${({ color, theme }) => (color ? color : theme.primary1)}; 101 | color: white; 102 | width: fit-content; 103 | border-radius: 12px; 104 | white-space: nowrap; 105 | 106 | :hover { 107 | background-color: ${({ color, theme }) => (color ? darken(0.1, color) : darken(0.1, theme.primary1))}; 108 | } 109 | ` 110 | 111 | export const ButtonFaded = styled(Base)` 112 | background-color: ${({ theme }) => theme.bg2}; 113 | color: (255, 255, 255, 0.5); 114 | white-space: nowrap; 115 | 116 | :hover { 117 | opacity: 0.5; 118 | } 119 | ` 120 | 121 | export function ButtonPlusDull({ disabled, children, ...rest }) { 122 | return ( 123 | 124 | 125 | 126 |
{children}
127 |
128 |
129 | ) 130 | } 131 | 132 | export function ButtonCustom({ children, bgColor, color, ...rest }) { 133 | return ( 134 | 135 | {children} 136 | 137 | ) 138 | } 139 | 140 | export const OptionButton = styled.div` 141 | font-weight: 500; 142 | width: fit-content; 143 | white-space: nowrap; 144 | padding: 6px; 145 | border-radius: 6px; 146 | border: 1px solid ${({ theme }) => theme.bg4}; 147 | background-color: ${({ active, theme }) => active && theme.bg3}; 148 | color: ${({ theme }) => theme.text1}; 149 | 150 | :hover { 151 | cursor: ${({ disabled }) => !disabled && 'pointer'}; 152 | } 153 | ` 154 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Text, Box } from 'rebass' 4 | 5 | import Link from './Link' 6 | 7 | import { urls } from '../utils' 8 | 9 | const Divider = styled(Box)` 10 | height: 1px; 11 | background-color: ${({ theme }) => theme.divider}; 12 | ` 13 | 14 | export const BlockedWrapper = styled.div` 15 | width: 100%; 16 | height: 100%; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | ` 21 | 22 | export const BlockedMessageWrapper = styled.div` 23 | border: 1px solid ${({ theme }) => theme.text3}; 24 | border-radius: 12px; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | padding: 1rem; 29 | max-width: 80%; 30 | ` 31 | 32 | export const IconWrapper = styled.div` 33 | position: absolute; 34 | right: 0; 35 | border-radius: 3px; 36 | height: 16px; 37 | width: 16px; 38 | padding: 0px; 39 | bottom: 0; 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | color: ${({ theme }) => theme.text1}; 44 | 45 | :hover { 46 | cursor: pointer; 47 | opacity: 0.7; 48 | } 49 | ` 50 | 51 | const Hint = ({ children, ...rest }) => ( 52 | 53 | {children} 54 | 55 | ) 56 | 57 | const Address = ({ address, token, ...rest }) => ( 58 | 65 | {address} 66 | 67 | ) 68 | 69 | export const Hover = styled.div` 70 | :hover { 71 | cursor: pointer; 72 | opacity: ${({ fade }) => fade && '0.7'}; 73 | } 74 | ` 75 | 76 | export const StyledIcon = styled.div` 77 | color: ${({ theme }) => theme.text1}; 78 | ` 79 | 80 | const EmptyCard = styled.div` 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | height: 200px; 85 | border-radius: 20px; 86 | color: ${({ theme }) => theme.text1}; 87 | height: ${({ height }) => height && height}; 88 | ` 89 | 90 | export const SideBar = styled.span` 91 | display: grid; 92 | grid-gap: 24px; 93 | position: sticky; 94 | top: 4rem; 95 | ` 96 | 97 | export const SubNav = styled.ul` 98 | list-style: none; 99 | display: flex; 100 | flex-direction: row; 101 | justify-content: flex-start; 102 | align-items: flex-start; 103 | padding: 0; 104 | margin-bottom: 2rem; 105 | ` 106 | export const SubNavEl = styled.li` 107 | list-style: none; 108 | display: flex; 109 | padding-bottom: 0.5rem; 110 | margin-right: 1rem; 111 | font-weight: ${({ isActive }) => (isActive ? 600 : 500)}; 112 | border-bottom: 1px solid rgba(0, 0, 0, 0); 113 | 114 | :hover { 115 | cursor: pointer; 116 | border-bottom: 1px solid ${({ theme }) => theme.bg3}; 117 | } 118 | ` 119 | 120 | export const PageWrapper = styled.div` 121 | display: flex; 122 | flex-direction: column; 123 | padding-top: 36px; 124 | padding-bottom: 80px; 125 | 126 | @media screen and (max-width: 600px) { 127 | & > * { 128 | padding: 0 12px; 129 | } 130 | } 131 | ` 132 | 133 | export const ContentWrapper = styled.div` 134 | display: grid; 135 | justify-content: start; 136 | align-items: start; 137 | grid-template-columns: 1fr; 138 | grid-gap: 24px; 139 | max-width: 1440px; 140 | width: 100%; 141 | margin: 0 auto; 142 | padding: 0 2rem; 143 | box-sizing: border-box; 144 | @media screen and (max-width: 1180px) { 145 | grid-template-columns: 1fr; 146 | padding: 0 1rem; 147 | } 148 | ` 149 | 150 | export const ContentWrapperLarge = styled.div` 151 | display: grid; 152 | justify-content: start; 153 | align-items: start; 154 | grid-template-columns: 1fr; 155 | grid-gap: 24px; 156 | padding: 0 2rem; 157 | margin: 0 auto; 158 | box-sizing: border-box; 159 | max-width: 1440px; 160 | width: 100%; 161 | 162 | @media screen and (max-width: 1282px) { 163 | grid-template-columns: 1fr; 164 | padding: 0 1rem; 165 | } 166 | ` 167 | 168 | export const FullWrapper = styled.div` 169 | display: grid; 170 | justify-content: start; 171 | align-items: start; 172 | grid-template-columns: 1fr; 173 | grid-gap: 24px; 174 | max-width: 1440px; 175 | width: 100%; 176 | margin: 0 auto; 177 | padding: 0 2rem; 178 | box-sizing: border-box; 179 | 180 | @media screen and (max-width: 1180px) { 181 | grid-template-columns: 1fr; 182 | padding: 0 1rem; 183 | } 184 | ` 185 | 186 | export const FixedMenu = styled.div` 187 | z-index: 99; 188 | width: 100%; 189 | box-sizing: border-box; 190 | padding: 1rem; 191 | box-sizing: border-box; 192 | margin-bottom: 2rem; 193 | max-width: 100vw; 194 | 195 | @media screen and (max-width: 800px) { 196 | margin-bottom: 0; 197 | } 198 | ` 199 | 200 | export { Hint, Divider, Address, EmptyCard } 201 | -------------------------------------------------------------------------------- /src/components/GlobalChart/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo, useEffect, useRef } from 'react' 2 | import { ResponsiveContainer } from 'recharts' 3 | import { timeframeOptions } from '../../constants' 4 | import { useGlobalChartData, useGlobalData } from '../../contexts/GlobalData' 5 | import { useMedia } from 'react-use' 6 | import DropdownSelect from '../DropdownSelect' 7 | import TradingViewChart, { CHART_TYPES } from '../TradingviewChart' 8 | import { RowFixed } from '../Row' 9 | import { OptionButton } from '../ButtonStyled' 10 | import { getTimeframe } from '../../utils' 11 | import { TYPE } from '../../Theme' 12 | 13 | const CHART_VIEW = { 14 | VOLUME: 'Volume', 15 | LIQUIDITY: 'Liquidity', 16 | } 17 | 18 | const VOLUME_WINDOW = { 19 | WEEKLY: 'WEEKLY', 20 | DAYS: 'DAYS', 21 | } 22 | const GlobalChart = ({ display }) => { 23 | // chart options 24 | const [chartView, setChartView] = useState(display === 'volume' ? CHART_VIEW.VOLUME : CHART_VIEW.LIQUIDITY) 25 | 26 | // time window and window size for chart 27 | const timeWindow = timeframeOptions.ALL_TIME 28 | const [volumeWindow, setVolumeWindow] = useState(VOLUME_WINDOW.DAYS) 29 | 30 | // global historical data 31 | const [dailyData, weeklyData] = useGlobalChartData() 32 | const { totalLiquidityUSD, oneDayVolumeUSD, volumeChangeUSD, liquidityChangeUSD, oneWeekVolume, weeklyVolumeChange } = 33 | useGlobalData() 34 | 35 | // based on window, get starttim 36 | let utcStartTime = getTimeframe(timeWindow) 37 | 38 | const chartDataFiltered = useMemo(() => { 39 | let currentData = volumeWindow === VOLUME_WINDOW.DAYS ? dailyData : weeklyData 40 | return ( 41 | currentData && 42 | Object.keys(currentData) 43 | ?.map((key) => { 44 | let item = currentData[key] 45 | if (item.date > utcStartTime) { 46 | return item 47 | } else { 48 | return true 49 | } 50 | }) 51 | .filter((item) => { 52 | return !!item 53 | }) 54 | ) 55 | }, [dailyData, utcStartTime, volumeWindow, weeklyData]) 56 | const below800 = useMedia('(max-width: 800px)') 57 | 58 | // update the width on a window resize 59 | const ref = useRef() 60 | const isClient = typeof window === 'object' 61 | const [width, setWidth] = useState(ref?.current?.container?.clientWidth) 62 | useEffect(() => { 63 | if (!isClient) { 64 | return false 65 | } 66 | function handleResize() { 67 | setWidth(ref?.current?.container?.clientWidth ?? width) 68 | } 69 | window.addEventListener('resize', handleResize) 70 | return () => window.removeEventListener('resize', handleResize) 71 | }, [isClient, width]) // Empty array ensures that effect is only run on mount and unmount 72 | 73 | return chartDataFiltered ? ( 74 | <> 75 | {below800 && ( 76 | 77 | )} 78 | 79 | {chartDataFiltered && chartView === CHART_VIEW.LIQUIDITY && ( 80 | 81 | 90 | 91 | )} 92 | {chartDataFiltered && chartView === CHART_VIEW.VOLUME && ( 93 | 94 | 104 | 105 | )} 106 | {display === 'volume' && ( 107 | 115 | setVolumeWindow(VOLUME_WINDOW.DAYS)} 118 | > 119 | D 120 | 121 | setVolumeWindow(VOLUME_WINDOW.WEEKLY)} 125 | > 126 | W 127 | 128 | 129 | )} 130 | 131 | ) : ( 132 | '' 133 | ) 134 | } 135 | 136 | export default GlobalChart 137 | -------------------------------------------------------------------------------- /src/components/UserChart/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from 'styled-components' 3 | import { Area, XAxis, YAxis, ResponsiveContainer, Tooltip, AreaChart } from 'recharts' 4 | import { AutoRow, RowBetween } from '../Row' 5 | import { toK, toNiceDate, toNiceDateYear, formattedNum, getTimeframe } from '../../utils' 6 | import { OptionButton } from '../ButtonStyled' 7 | import { darken } from 'polished' 8 | import { useMedia } from 'react-use' 9 | import { timeframeOptions } from '../../constants' 10 | import DropdownSelect from '../DropdownSelect' 11 | import { useUserLiquidityChart } from '../../contexts/User' 12 | import LocalLoader from '../LocalLoader' 13 | import { useDarkModeManager } from '../../contexts/LocalStorage' 14 | import { TYPE } from '../../Theme' 15 | 16 | const ChartWrapper = styled.div` 17 | height: 100%; 18 | max-height: 390px; 19 | 20 | @media screen and (max-width: 600px) { 21 | min-height: 200px; 22 | } 23 | ` 24 | 25 | const UserChart = ({ account }) => { 26 | const chartData = useUserLiquidityChart(account) 27 | 28 | const [timeWindow, setTimeWindow] = useState(timeframeOptions.ALL_TIME) 29 | let utcStartTime = getTimeframe(timeWindow) 30 | 31 | const below600 = useMedia('(max-width: 600px)') 32 | const above1600 = useMedia('(min-width: 1600px)') 33 | 34 | const domain = [(dataMin) => (dataMin > utcStartTime ? dataMin : utcStartTime), 'dataMax'] 35 | 36 | const aspect = above1600 ? 60 / 12 : below600 ? 60 / 42 : 60 / 16 37 | 38 | const [darkMode] = useDarkModeManager() 39 | const textColor = darkMode ? 'white' : 'black' 40 | 41 | return ( 42 | 43 | {below600 ? ( 44 | 45 |
46 | 47 | 48 | ) : ( 49 | 50 | 51 | Liquidity Value 52 | 53 | 54 | setTimeWindow(timeframeOptions.MONTH)} 57 | > 58 | 1M 59 | 60 | setTimeWindow(timeframeOptions.WEEK)} 63 | > 64 | 1W 65 | 66 | setTimeWindow(timeframeOptions.ALL_TIME)} 69 | > 70 | All 71 | 72 | 73 | 74 | )} 75 | {chartData ? ( 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | toNiceDate(tick)} 91 | dataKey="date" 92 | tick={{ fill: textColor }} 93 | type={'number'} 94 | domain={domain} 95 | /> 96 | '$' + toK(tick)} 100 | axisLine={false} 101 | tickLine={false} 102 | interval="preserveEnd" 103 | minTickGap={6} 104 | yAxisId={0} 105 | tick={{ fill: textColor }} 106 | /> 107 | formattedNum(val, true)} 110 | labelFormatter={(label) => toNiceDateYear(label)} 111 | labelStyle={{ paddingTop: 4 }} 112 | contentStyle={{ 113 | padding: '10px 14px', 114 | borderRadius: 10, 115 | borderColor: '#ff007a', 116 | color: 'black', 117 | }} 118 | wrapperStyle={{ top: -70, left: -10 }} 119 | /> 120 | 132 | 133 | 134 | ) : ( 135 | 136 | )} 137 | 138 | ) 139 | } 140 | 141 | export default UserChart 142 | -------------------------------------------------------------------------------- /src/contexts/LocalStorage.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react' 2 | 3 | const UNISWAP = 'UNISWAP' 4 | 5 | const VERSION = 'VERSION' 6 | const CURRENT_VERSION = 0 7 | const LAST_SAVED = 'LAST_SAVED' 8 | const DISMISSED_PATHS = 'DISMISSED_PATHS' 9 | const SAVED_ACCOUNTS = 'SAVED_ACCOUNTS' 10 | const SAVED_TOKENS = 'SAVED_TOKENS' 11 | const SAVED_PAIRS = 'SAVED_PAIRS' 12 | 13 | const DARK_MODE = 'DARK_MODE' 14 | 15 | const UPDATABLE_KEYS = [DARK_MODE, DISMISSED_PATHS, SAVED_ACCOUNTS, SAVED_PAIRS, SAVED_TOKENS] 16 | 17 | const UPDATE_KEY = 'UPDATE_KEY' 18 | 19 | const LocalStorageContext = createContext() 20 | 21 | function useLocalStorageContext() { 22 | return useContext(LocalStorageContext) 23 | } 24 | 25 | function reducer(state, { type, payload }) { 26 | switch (type) { 27 | case UPDATE_KEY: { 28 | const { key, value } = payload 29 | if (!UPDATABLE_KEYS.some((k) => k === key)) { 30 | throw Error(`Unexpected key in LocalStorageContext reducer: '${key}'.`) 31 | } else { 32 | return { 33 | ...state, 34 | [key]: value, 35 | } 36 | } 37 | } 38 | default: { 39 | throw Error(`Unexpected action type in LocalStorageContext reducer: '${type}'.`) 40 | } 41 | } 42 | } 43 | 44 | function init() { 45 | const defaultLocalStorage = { 46 | [VERSION]: CURRENT_VERSION, 47 | [DARK_MODE]: true, 48 | [DISMISSED_PATHS]: {}, 49 | [SAVED_ACCOUNTS]: [], 50 | [SAVED_TOKENS]: {}, 51 | [SAVED_PAIRS]: {}, 52 | } 53 | 54 | try { 55 | const parsed = JSON.parse(window.localStorage.getItem(UNISWAP)) 56 | if (parsed[VERSION] !== CURRENT_VERSION) { 57 | // this is where we could run migration logic 58 | return defaultLocalStorage 59 | } else { 60 | return { ...defaultLocalStorage, ...parsed } 61 | } 62 | } catch { 63 | return defaultLocalStorage 64 | } 65 | } 66 | 67 | export default function Provider({ children }) { 68 | const [state, dispatch] = useReducer(reducer, undefined, init) 69 | 70 | const updateKey = useCallback((key, value) => { 71 | dispatch({ type: UPDATE_KEY, payload: { key, value } }) 72 | }, []) 73 | 74 | return ( 75 | [state, { updateKey }], [state, updateKey])}> 76 | {children} 77 | 78 | ) 79 | } 80 | 81 | export function Updater() { 82 | const [state] = useLocalStorageContext() 83 | 84 | useEffect(() => { 85 | window.localStorage.setItem(UNISWAP, JSON.stringify({ ...state, [LAST_SAVED]: Math.floor(Date.now() / 1000) })) 86 | }) 87 | 88 | return null 89 | } 90 | 91 | export function useDarkModeManager() { 92 | const [state, { updateKey }] = useLocalStorageContext() 93 | let isDarkMode = state[DARK_MODE] 94 | const toggleDarkMode = useCallback( 95 | (value) => { 96 | updateKey(DARK_MODE, value === false || value === true ? value : !isDarkMode) 97 | }, 98 | [updateKey, isDarkMode] 99 | ) 100 | return [isDarkMode, toggleDarkMode] 101 | } 102 | 103 | export function usePathDismissed(path) { 104 | const [state, { updateKey }] = useLocalStorageContext() 105 | const pathDismissed = state?.[DISMISSED_PATHS]?.[path] 106 | function dismiss() { 107 | let newPaths = state?.[DISMISSED_PATHS] 108 | newPaths[path] = true 109 | updateKey(DISMISSED_PATHS, newPaths) 110 | } 111 | 112 | return [pathDismissed, dismiss] 113 | } 114 | 115 | export function useSavedAccounts() { 116 | const [state, { updateKey }] = useLocalStorageContext() 117 | const savedAccounts = state?.[SAVED_ACCOUNTS] 118 | 119 | const addAccount = useCallback( 120 | (account) => { 121 | updateKey(SAVED_ACCOUNTS, [...(savedAccounts ?? []), account]) 122 | }, 123 | [savedAccounts, updateKey] 124 | ) 125 | 126 | const removeAccount = useCallback( 127 | (account) => { 128 | let index = savedAccounts?.indexOf(account) ?? -1 129 | if (index > -1) { 130 | updateKey(SAVED_ACCOUNTS, [ 131 | ...savedAccounts.slice(0, index), 132 | ...savedAccounts.slice(index + 1, savedAccounts.length), 133 | ]) 134 | } 135 | }, 136 | [savedAccounts, updateKey] 137 | ) 138 | 139 | return [savedAccounts, addAccount, removeAccount] 140 | } 141 | 142 | export function useSavedPairs() { 143 | const [state, { updateKey }] = useLocalStorageContext() 144 | const savedPairs = state?.[SAVED_PAIRS] 145 | 146 | function addPair(address, token0Address, token1Address, token0Symbol, token1Symbol) { 147 | let newList = state?.[SAVED_PAIRS] 148 | newList[address] = { 149 | address, 150 | token0Address, 151 | token1Address, 152 | token0Symbol, 153 | token1Symbol, 154 | } 155 | updateKey(SAVED_PAIRS, newList) 156 | } 157 | 158 | function removePair(address) { 159 | let newList = state?.[SAVED_PAIRS] 160 | newList[address] = null 161 | updateKey(SAVED_PAIRS, newList) 162 | } 163 | 164 | return [savedPairs, addPair, removePair] 165 | } 166 | 167 | export function useSavedTokens() { 168 | const [state, { updateKey }] = useLocalStorageContext() 169 | const savedTokens = state?.[SAVED_TOKENS] 170 | 171 | function addToken(address, symbol) { 172 | let newList = state?.[SAVED_TOKENS] 173 | newList[address] = { 174 | symbol, 175 | } 176 | updateKey(SAVED_TOKENS, newList) 177 | } 178 | 179 | function removeToken(address) { 180 | let newList = state?.[SAVED_TOKENS] 181 | newList[address] = null 182 | updateKey(SAVED_TOKENS, newList) 183 | } 184 | 185 | return [savedTokens, addToken, removeToken] 186 | } 187 | -------------------------------------------------------------------------------- /src/components/PinnedData/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withRouter } from 'react-router-dom' 3 | import styled from 'styled-components' 4 | import { RowBetween, RowFixed } from '../Row' 5 | import { AutoColumn } from '../Column' 6 | import { TYPE } from '../../Theme' 7 | import { useSavedTokens, useSavedPairs } from '../../contexts/LocalStorage' 8 | import { Hover } from '..' 9 | import TokenLogo from '../TokenLogo' 10 | import AccountSearch from '../AccountSearch' 11 | import { Bookmark, ChevronRight, X } from 'react-feather' 12 | import { ButtonFaded } from '../ButtonStyled' 13 | import FormattedName from '../FormattedName' 14 | 15 | const RightColumn = styled.div` 16 | position: fixed; 17 | right: 0; 18 | top: 0px; 19 | height: 100vh; 20 | width: ${({ open }) => (open ? '160px' : '23px')}; 21 | padding: 1.25rem; 22 | border-left: ${({ theme, open }) => '1px solid' + theme.bg3}; 23 | background-color: ${({ theme }) => theme.bg1}; 24 | z-index: 9999; 25 | overflow: auto; 26 | :hover { 27 | cursor: pointer; 28 | } 29 | ` 30 | 31 | const SavedButton = styled(RowBetween)` 32 | padding-bottom: ${({ open }) => open && '20px'}; 33 | border-bottom: ${({ theme, open }) => open && '1px solid' + theme.bg3}; 34 | margin-bottom: ${({ open }) => open && '1.25rem'}; 35 | 36 | :hover { 37 | cursor: pointer; 38 | } 39 | ` 40 | 41 | const ScrollableDiv = styled(AutoColumn)` 42 | overflow: auto; 43 | padding-bottom: 60px; 44 | ` 45 | 46 | const StyledIcon = styled.div` 47 | color: ${({ theme }) => theme.text2}; 48 | ` 49 | 50 | function PinnedData({ history, open, setSavedOpen }) { 51 | const [savedPairs, , removePair] = useSavedPairs() 52 | const [savedTokens, , removeToken] = useSavedTokens() 53 | 54 | return !open ? ( 55 | setSavedOpen(true)}> 56 | 57 | 58 | 59 | 60 | 61 | 62 | ) : ( 63 | 64 | setSavedOpen(false)} open={open}> 65 | 66 | 67 | 68 | 69 | Saved 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Pinned Pairs 79 | {Object.keys(savedPairs).filter((key) => { 80 | return !!savedPairs[key] 81 | }).length > 0 ? ( 82 | Object.keys(savedPairs) 83 | .filter((address) => { 84 | return !!savedPairs[address] 85 | }) 86 | .map((address) => { 87 | const pair = savedPairs[address] 88 | return ( 89 | 90 | history.push('/pair/' + address)}> 91 | 92 | 93 | 98 | 99 | 100 | 101 | removePair(pair.address)}> 102 | 103 | 104 | 105 | 106 | 107 | ) 108 | }) 109 | ) : ( 110 | Pinned pairs will appear here. 111 | )} 112 | 113 | 114 | Pinned Tokens 115 | {Object.keys(savedTokens).filter((key) => { 116 | return !!savedTokens[key] 117 | }).length > 0 ? ( 118 | Object.keys(savedTokens) 119 | .filter((address) => { 120 | return !!savedTokens[address] 121 | }) 122 | .map((address) => { 123 | const token = savedTokens[address] 124 | return ( 125 | 126 | history.push('/token/' + address)}> 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | removeToken(address)}> 135 | 136 | 137 | 138 | 139 | 140 | ) 141 | }) 142 | ) : ( 143 | Pinned tokens will appear here. 144 | )} 145 | 146 | 147 | 148 | ) 149 | } 150 | 151 | export default withRouter(PinnedData) 152 | -------------------------------------------------------------------------------- /src/components/AccountSearch/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import 'feather-icons' 3 | import { withRouter } from 'react-router-dom' 4 | import styled from 'styled-components' 5 | import { ButtonLight, ButtonFaded } from '../ButtonStyled' 6 | import { AutoRow, RowBetween } from '../Row' 7 | import { isAddress } from '../../utils' 8 | import { useSavedAccounts } from '../../contexts/LocalStorage' 9 | import { AutoColumn } from '../Column' 10 | import { TYPE } from '../../Theme' 11 | import { Hover, StyledIcon } from '..' 12 | import Panel from '../Panel' 13 | import { Divider } from '..' 14 | import { Flex } from 'rebass' 15 | 16 | import { X } from 'react-feather' 17 | 18 | const Wrapper = styled.div` 19 | display: flex; 20 | flex-direction: row; 21 | align-items: center; 22 | justify-content: flex-end; 23 | width: 100%; 24 | border-radius: 12px; 25 | ` 26 | 27 | const Input = styled.input` 28 | position: relative; 29 | display: flex; 30 | align-items: center; 31 | width: 100%; 32 | white-space: nowrap; 33 | background: none; 34 | border: none; 35 | outline: none; 36 | padding: 12px 16px; 37 | border-radius: 12px; 38 | color: ${({ theme }) => theme.text1}; 39 | background-color: ${({ theme }) => theme.bg1}; 40 | font-size: 16px; 41 | margin-right: 1rem; 42 | border: 1px solid ${({ theme }) => theme.bg3}; 43 | 44 | ::placeholder { 45 | color: ${({ theme }) => theme.text3}; 46 | font-size: 14px; 47 | } 48 | 49 | @media screen and (max-width: 640px) { 50 | ::placeholder { 51 | font-size: 1rem; 52 | } 53 | } 54 | ` 55 | 56 | const AccountLink = styled.span` 57 | display: flex; 58 | cursor: pointer; 59 | color: ${({ theme }) => theme.link}; 60 | font-size: 14px; 61 | font-weight: 500; 62 | ` 63 | 64 | const DashGrid = styled.div` 65 | display: grid; 66 | grid-gap: 1em; 67 | grid-template-columns: 1fr; 68 | grid-template-areas: 'account'; 69 | padding: 0 4px; 70 | 71 | > * { 72 | justify-content: flex-end; 73 | } 74 | ` 75 | 76 | function AccountSearch({ history, small }) { 77 | const [accountValue, setAccountValue] = useState() 78 | const [savedAccounts, addAccount, removeAccount] = useSavedAccounts() 79 | 80 | function handleAccountSearch() { 81 | if (isAddress(accountValue)) { 82 | history.push('/account/' + accountValue) 83 | if (!savedAccounts.includes(accountValue)) { 84 | addAccount(accountValue) 85 | } 86 | } 87 | } 88 | 89 | return ( 90 | 91 | {!small && ( 92 | <> 93 | 94 | 95 | { 98 | setAccountValue(e.target.value) 99 | }} 100 | /> 101 | 102 | Load Account Details 103 | 104 | 105 | )} 106 | 107 | 108 | {!small && ( 109 | 110 | 111 | Saved Accounts 112 | 113 | 114 | {savedAccounts?.length > 0 ? ( 115 | savedAccounts.map((account) => { 116 | return ( 117 | 118 | history.push('/account/' + account)} 122 | > 123 | {account?.slice(0, 42)} 124 | { 126 | e.stopPropagation() 127 | removeAccount(account) 128 | }} 129 | > 130 | 131 | 132 | 133 | 134 | 135 | 136 | ) 137 | }) 138 | ) : ( 139 | No saved accounts 140 | )} 141 | 142 | )} 143 | 144 | {small && ( 145 | <> 146 | {'Accounts'} 147 | {savedAccounts?.length > 0 ? ( 148 | savedAccounts.map((account) => { 149 | return ( 150 | 151 | history.push('/account/' + account)}> 152 | {small ? ( 153 | {account?.slice(0, 6) + '...' + account?.slice(38, 42)} 154 | ) : ( 155 | {account?.slice(0, 42)} 156 | )} 157 | 158 | removeAccount(account)}> 159 | 160 | 161 | 162 | 163 | 164 | ) 165 | }) 166 | ) : ( 167 | No pinned wallets 168 | )} 169 | 170 | )} 171 | 172 | 173 | ) 174 | } 175 | 176 | export default withRouter(AccountSearch) 177 | -------------------------------------------------------------------------------- /src/components/PairReturnsChart/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from 'styled-components' 3 | import { XAxis, YAxis, ResponsiveContainer, Tooltip, LineChart, Line, CartesianGrid } from 'recharts' 4 | import { AutoRow, RowBetween } from '../Row' 5 | 6 | import { toK, toNiceDate, toNiceDateYear, formattedNum, getTimeframe } from '../../utils' 7 | import { OptionButton } from '../ButtonStyled' 8 | import { useMedia } from 'react-use' 9 | import { timeframeOptions } from '../../constants' 10 | import DropdownSelect from '../DropdownSelect' 11 | import { useUserPositionChart } from '../../contexts/User' 12 | import { useTimeframe } from '../../contexts/Application' 13 | import LocalLoader from '../LocalLoader' 14 | import { useColor } from '../../hooks' 15 | import { useDarkModeManager } from '../../contexts/LocalStorage' 16 | 17 | const ChartWrapper = styled.div` 18 | max-height: 420px; 19 | 20 | @media screen and (max-width: 600px) { 21 | min-height: 200px; 22 | } 23 | ` 24 | 25 | const OptionsRow = styled.div` 26 | display: flex; 27 | flex-direction: row; 28 | width: 100%; 29 | margin-bottom: 40px; 30 | ` 31 | 32 | const CHART_VIEW = { 33 | VALUE: 'Value', 34 | FEES: 'Fees', 35 | } 36 | 37 | const PairReturnsChart = ({ account, position }) => { 38 | let data = useUserPositionChart(position, account) 39 | 40 | const [timeWindow, setTimeWindow] = useTimeframe() 41 | 42 | const below600 = useMedia('(max-width: 600px)') 43 | 44 | const color = useColor(position?.pair.token0.id) 45 | 46 | const [chartView, setChartView] = useState(CHART_VIEW.VALUE) 47 | 48 | // based on window, get starttime 49 | let utcStartTime = getTimeframe(timeWindow) 50 | data = data?.filter((entry) => entry.date >= utcStartTime) 51 | 52 | const aspect = below600 ? 60 / 42 : 60 / 16 53 | 54 | const [darkMode] = useDarkModeManager() 55 | const textColor = darkMode ? 'white' : 'black' 56 | 57 | return ( 58 | 59 | {below600 ? ( 60 | 61 |
62 | 63 | 64 | ) : ( 65 | 66 | 67 | setChartView(CHART_VIEW.VALUE)}> 68 | Liquidity 69 | 70 | setChartView(CHART_VIEW.FEES)}> 71 | Fees 72 | 73 | 74 | 75 | setTimeWindow(timeframeOptions.WEEK)} 78 | > 79 | 1W 80 | 81 | setTimeWindow(timeframeOptions.MONTH)} 84 | > 85 | 1M 86 | 87 | setTimeWindow(timeframeOptions.ALL_TIME)} 90 | > 91 | All 92 | 93 | 94 | 95 | )} 96 | 97 | {data ? ( 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | toNiceDate(tick)} 112 | dataKey="date" 113 | tick={{ fill: textColor }} 114 | type={'number'} 115 | domain={['dataMin', 'dataMax']} 116 | /> 117 | '$' + toK(tick)} 121 | axisLine={false} 122 | tickLine={false} 123 | interval="preserveStartEnd" 124 | minTickGap={0} 125 | yAxisId={0} 126 | tick={{ fill: textColor }} 127 | /> 128 | formattedNum(val, true)} 131 | labelFormatter={(label) => toNiceDateYear(label)} 132 | labelStyle={{ paddingTop: 4 }} 133 | contentStyle={{ 134 | padding: '10px 14px', 135 | borderRadius: 10, 136 | borderColor: color, 137 | color: 'black', 138 | }} 139 | wrapperStyle={{ top: -70, left: -10 }} 140 | /> 141 | 142 | 149 | 150 | ) : ( 151 | 152 | )} 153 | 154 | 155 | ) 156 | } 157 | 158 | export default PairReturnsChart 159 | -------------------------------------------------------------------------------- /src/components/LPList/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { useMedia } from 'react-use' 3 | import dayjs from 'dayjs' 4 | import LocalLoader from '../LocalLoader' 5 | import utc from 'dayjs/plugin/utc' 6 | import { Box, Flex } from 'rebass' 7 | import styled from 'styled-components' 8 | 9 | import { CustomLink } from '../Link' 10 | import { Divider } from '..' 11 | import { withRouter } from 'react-router-dom' 12 | import { formattedNum } from '../../utils' 13 | import { TYPE } from '../../Theme' 14 | import DoubleTokenLogo from '../DoubleLogo' 15 | import { RowFixed } from '../Row' 16 | 17 | dayjs.extend(utc) 18 | 19 | const PageButtons = styled.div` 20 | width: 100%; 21 | display: flex; 22 | justify-content: center; 23 | margin-top: 2em; 24 | margin-bottom: 0.5em; 25 | ` 26 | 27 | const Arrow = styled.div` 28 | color: ${({ theme }) => theme.primary1}; 29 | opacity: ${(props) => (props.faded ? 0.3 : 1)}; 30 | padding: 0 20px; 31 | user-select: none; 32 | :hover { 33 | cursor: pointer; 34 | } 35 | ` 36 | 37 | const List = styled(Box)` 38 | -webkit-overflow-scrolling: touch; 39 | ` 40 | 41 | const DashGrid = styled.div` 42 | display: grid; 43 | grid-gap: 1em; 44 | grid-template-columns: 10px 1.5fr 1fr 1fr; 45 | grid-template-areas: 'number name pair value'; 46 | padding: 0 4px; 47 | 48 | > * { 49 | justify-content: flex-end; 50 | } 51 | 52 | @media screen and (max-width: 1080px) { 53 | grid-template-columns: 10px 1.5fr 1fr 1fr; 54 | grid-template-areas: 'number name pair value'; 55 | } 56 | 57 | @media screen and (max-width: 600px) { 58 | grid-template-columns: 1fr 1fr 1fr; 59 | grid-template-areas: 'name pair value'; 60 | } 61 | ` 62 | 63 | const ListWrapper = styled.div`` 64 | 65 | const DataText = styled(Flex)` 66 | align-items: center; 67 | text-align: center; 68 | color: ${({ theme }) => theme.text1}; 69 | & > * { 70 | font-size: 14px; 71 | } 72 | 73 | @media screen and (max-width: 600px) { 74 | font-size: 13px; 75 | } 76 | ` 77 | 78 | function LPList({ lps, disbaleLinks, maxItems = 10 }) { 79 | const below600 = useMedia('(max-width: 600px)') 80 | const below800 = useMedia('(max-width: 800px)') 81 | 82 | // pagination 83 | const [page, setPage] = useState(1) 84 | const [maxPage, setMaxPage] = useState(1) 85 | const ITEMS_PER_PAGE = maxItems 86 | 87 | useEffect(() => { 88 | setMaxPage(1) // edit this to do modular 89 | setPage(1) 90 | }, [lps]) 91 | 92 | useEffect(() => { 93 | if (lps) { 94 | let extraPages = 1 95 | if (Object.keys(lps).length % ITEMS_PER_PAGE === 0) { 96 | extraPages = 0 97 | } 98 | setMaxPage(Math.floor(Object.keys(lps).length / ITEMS_PER_PAGE) + extraPages) 99 | } 100 | }, [ITEMS_PER_PAGE, lps]) 101 | 102 | const ListItem = ({ lp, index }) => { 103 | return ( 104 | 105 | {!below600 && ( 106 | 107 | {index} 108 | 109 | )} 110 | 111 | 112 | {below800 ? lp.user.id.slice(0, 4) + '...' + lp.user.id.slice(38, 42) : lp.user.id} 113 | 114 | 115 | 116 | {/* {!below1080 && ( 117 | 118 | {lp.type} 119 | 120 | )} */} 121 | 122 | 123 | 124 | 125 | {!below600 && } 126 | {lp.pairName} 127 | 128 | 129 | 130 | {formattedNum(lp.usd, true)} 131 | 132 | ) 133 | } 134 | 135 | const lpList = 136 | lps && 137 | lps.slice(ITEMS_PER_PAGE * (page - 1), page * ITEMS_PER_PAGE).map((lp, index) => { 138 | return ( 139 |
140 | 141 | 142 |
143 | ) 144 | }) 145 | 146 | return ( 147 | 148 | 149 | {!below600 && ( 150 | 151 | # 152 | 153 | )} 154 | 155 | Account 156 | 157 | {/* {!below1080 && ( 158 | 159 | Type 160 | 161 | )} */} 162 | 163 | Pair 164 | 165 | 166 | Value 167 | 168 | 169 | 170 | {!lpList ? : lpList} 171 | 172 |
setPage(page === 1 ? page : page - 1)}> 173 | 174 |
175 | {'Page ' + page + ' of ' + maxPage} 176 |
setPage(page === maxPage ? page : page + 1)}> 177 | 178 |
179 |
180 |
181 | ) 182 | } 183 | 184 | export default withRouter(LPList) 185 | -------------------------------------------------------------------------------- /src/components/Select/styles.js: -------------------------------------------------------------------------------- 1 | import theme from '../Theme/theme' 2 | const color = theme.colors 3 | 4 | export const customStyles = { 5 | control: (styles, state) => ({ 6 | ...styles, 7 | borderRadius: 20, 8 | backgroundColor: 'white', 9 | color: '#6C7284', 10 | maxHeight: '32px', 11 | margin: 0, 12 | padding: 0, 13 | border: 'none', 14 | boxShadow: 'none', 15 | ':hover': { 16 | borderColor: color.zircon, 17 | cursor: 'pointer', 18 | overflow: 'hidden', 19 | }, 20 | }), 21 | placeholder: (styles) => ({ 22 | ...styles, 23 | color: '#6C7284', 24 | }), 25 | input: (styles) => ({ 26 | ...styles, 27 | color: '#6C7284', 28 | overflow: 'hidden', 29 | }), 30 | singleValue: (styles) => ({ 31 | ...styles, 32 | color: '#6C7284', 33 | width: '100%', 34 | paddingRight: '8px', 35 | }), 36 | indicatorSeparator: () => ({ 37 | display: 'none', 38 | }), 39 | dropdownIndicator: (styles) => ({ 40 | ...styles, 41 | color: '#6C7284', 42 | paddingRight: 0, 43 | }), 44 | valueContainer: (styles) => ({ 45 | ...styles, 46 | paddingLeft: 16, 47 | textAlign: 'right', 48 | overflow: 'scroll', 49 | }), 50 | menuPlacer: (styles) => ({ 51 | ...styles, 52 | }), 53 | option: (styles, state) => ({ 54 | ...styles, 55 | margin: '0px 0px', 56 | padding: 'calc(12px - 1px) calc(12px - 1px)', 57 | width: '', 58 | lineHeight: 1, 59 | color: state.isSelected ? '#000' : '', 60 | border: state.isSelected ? '1px solid var(--c-zircon)' : '1px solid transparent', 61 | borderRadius: state.isSelected && 30, 62 | backgroundColor: state.isSelected ? 'var(--c-alabaster)' : '', 63 | ':hover': { 64 | backgroundColor: 'var(--c-alabaster)', 65 | cursor: 'pointer', 66 | }, 67 | }), 68 | menu: (styles) => ({ 69 | ...styles, 70 | borderRadius: 16, 71 | boxShadow: '0 4px 8px 0 rgba(47, 128, 237, 0.1), 0 0 0 0.5px var(--c-zircon)', 72 | overflow: 'hidden', 73 | padding: 0, 74 | }), 75 | menuList: (styles) => ({ 76 | ...styles, 77 | color: color.text, 78 | padding: 0, 79 | }), 80 | } 81 | 82 | export const customStylesMobile = { 83 | control: (styles, state) => ({ 84 | ...styles, 85 | borderRadius: 12, 86 | backgroundColor: 'white', 87 | color: '#6C7284', 88 | maxHeight: '32px', 89 | margin: 0, 90 | padding: 0, 91 | boxShadow: 'none', 92 | ':hover': { 93 | borderColor: color.zircon, 94 | cursor: 'pointer', 95 | }, 96 | }), 97 | placeholder: (styles) => ({ 98 | ...styles, 99 | color: '#6C7284', 100 | }), 101 | input: (styles) => ({ 102 | ...styles, 103 | color: '6C7284', 104 | overflow: 'hidden', 105 | }), 106 | singleValue: (styles) => ({ 107 | ...styles, 108 | color: '#6C7284', 109 | }), 110 | indicatorSeparator: () => ({ 111 | display: 'none', 112 | }), 113 | dropdownIndicator: (styles) => ({ 114 | ...styles, 115 | paddingRight: 0, 116 | }), 117 | valueContainer: (styles) => ({ 118 | ...styles, 119 | paddingLeft: 16, 120 | }), 121 | menuPlacer: (styles) => ({ 122 | ...styles, 123 | }), 124 | option: (styles, state) => ({ 125 | ...styles, 126 | margin: '20px 4px', 127 | padding: 'calc(16px - 1px) 16x', 128 | width: '', 129 | lineHeight: 1, 130 | color: state.isSelected ? '#000' : '', 131 | // border: state.isSelected ? '1px solid var(--c-zircon)' : '1px solid transparent', 132 | borderRadius: state.isSelected && 30, 133 | backgroundColor: state.isSelected ? 'var(--c-alabaster)' : '', 134 | ':hover': { 135 | backgroundColor: 'var(--c-alabaster)', 136 | cursor: 'pointer', 137 | }, 138 | }), 139 | menu: (styles) => ({ 140 | ...styles, 141 | borderRadius: 20, 142 | boxShadow: '0 4px 8px 0 rgba(47, 128, 237, 0.1), 0 0 0 0.5px var(--c-zircon)', 143 | overflow: 'hidden', 144 | paddingBottom: '12px', 145 | }), 146 | menuList: (styles) => ({ 147 | ...styles, 148 | color: color.text, 149 | padding: '8px', 150 | }), 151 | } 152 | 153 | export const customStylesTime = { 154 | control: (styles, state) => ({ 155 | ...styles, 156 | borderRadius: 20, 157 | backgroundColor: 'white', 158 | color: '#6C7284', 159 | maxHeight: '32px', 160 | margin: 0, 161 | padding: 0, 162 | border: 'none', 163 | boxShadow: 'none', 164 | ':hover': { 165 | borderColor: color.zircon, 166 | cursor: 'pointer', 167 | }, 168 | }), 169 | placeholder: (styles) => ({ 170 | ...styles, 171 | color: '#6C7284', 172 | }), 173 | input: (styles) => ({ 174 | ...styles, 175 | color: 'transparent', 176 | }), 177 | singleValue: (styles) => ({ 178 | ...styles, 179 | color: '#6C7284', 180 | width: '100%', 181 | paddingRight: '8px', 182 | }), 183 | indicatorSeparator: () => ({ 184 | display: 'none', 185 | }), 186 | dropdownIndicator: (styles) => ({ 187 | ...styles, 188 | color: '#6C7284', 189 | paddingRight: 0, 190 | }), 191 | valueContainer: (styles) => ({ 192 | ...styles, 193 | paddingLeft: 16, 194 | overflow: 'visible', 195 | textAlign: 'right', 196 | }), 197 | menuPlacer: (styles) => ({ 198 | ...styles, 199 | }), 200 | option: (styles, state) => ({ 201 | ...styles, 202 | margin: '0px 0px', 203 | padding: 'calc(12px - 1px) calc(24px - 1px)', 204 | width: '', 205 | lineHeight: 1, 206 | color: state.isSelected ? '#000' : '', 207 | border: state.isSelected ? '1px solid var(--c-zircon)' : '1px solid transparent', 208 | borderRadius: state.isSelected && 30, 209 | backgroundColor: state.isSelected ? 'var(--c-alabaster)' : '', 210 | ':hover': { 211 | backgroundColor: 'var(--c-alabaster)', 212 | cursor: 'pointer', 213 | }, 214 | }), 215 | menu: (styles) => ({ 216 | ...styles, 217 | borderRadius: 16, 218 | boxShadow: '0 4px 8px 0 rgba(47, 128, 237, 0.1), 0 0 0 0.5px var(--c-zircon)', 219 | overflow: 'hidden', 220 | padding: 0, 221 | }), 222 | menuList: (styles) => ({ 223 | ...styles, 224 | color: color.text, 225 | padding: 0, 226 | }), 227 | } 228 | 229 | export default customStyles 230 | -------------------------------------------------------------------------------- /src/Theme/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle } from 'styled-components' 3 | import { useDarkModeManager } from '../contexts/LocalStorage' 4 | import styled from 'styled-components' 5 | import { Text } from 'rebass' 6 | 7 | export default function ThemeProvider({ children }) { 8 | const [darkMode] = useDarkModeManager() 9 | 10 | return {children} 11 | } 12 | 13 | const theme = (darkMode, color) => ({ 14 | customColor: color, 15 | textColor: darkMode ? color : 'black', 16 | 17 | panelColor: darkMode ? 'rgba(255, 255, 255, 0)' : 'rgba(255, 255, 255, 0)', 18 | backgroundColor: darkMode ? '#212429' : '#F7F8FA', 19 | 20 | uniswapPink: darkMode ? '#ff007a' : 'black', 21 | 22 | concreteGray: darkMode ? '#292C2F' : '#FAFAFA', 23 | inputBackground: darkMode ? '#1F1F1F' : '#FAFAFA', 24 | shadowColor: darkMode ? '#000' : '#2F80ED', 25 | mercuryGray: darkMode ? '#333333' : '#E1E1E1', 26 | 27 | text1: darkMode ? '#FAFAFA' : '#1F1F1F', 28 | text2: darkMode ? '#C3C5CB' : '#565A69', 29 | text3: darkMode ? '#6C7284' : '#888D9B', 30 | text4: darkMode ? '#565A69' : '#C3C5CB', 31 | text5: darkMode ? '#2C2F36' : '#EDEEF2', 32 | 33 | // special case text types 34 | white: '#FFFFFF', 35 | 36 | // backgrounds / greys 37 | bg1: darkMode ? '#212429' : '#FAFAFA', 38 | bg2: darkMode ? '#2C2F36' : '#F7F8FA', 39 | bg3: darkMode ? '#40444F' : '#EDEEF2', 40 | bg4: darkMode ? '#565A69' : '#CED0D9', 41 | bg5: darkMode ? '#565A69' : '#888D9B', 42 | bg6: darkMode ? '#000' : '#FFFFFF', 43 | 44 | //specialty colors 45 | modalBG: darkMode ? 'rgba(0,0,0,0.85)' : 'rgba(0,0,0,0.6)', 46 | advancedBG: darkMode ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.4)', 47 | onlyLight: darkMode ? '#22242a' : 'transparent', 48 | divider: darkMode ? 'rgba(43, 43, 43, 0.435)' : 'rgba(43, 43, 43, 0.035)', 49 | 50 | //primary colors 51 | primary1: darkMode ? '#2172E5' : '#ff007a', 52 | primary2: darkMode ? '#3680E7' : '#FF8CC3', 53 | primary3: darkMode ? '#4D8FEA' : '#FF99C9', 54 | primary4: darkMode ? '#376bad70' : '#F6DDE8', 55 | primary5: darkMode ? '#153d6f70' : '#FDEAF1', 56 | 57 | // color text 58 | primaryText1: darkMode ? '#6da8ff' : '#ff007a', 59 | 60 | // secondary colors 61 | secondary1: darkMode ? '#2172E5' : '#ff007a', 62 | secondary2: darkMode ? '#17000b26' : '#F6DDE8', 63 | secondary3: darkMode ? '#17000b26' : '#FDEAF1', 64 | 65 | shadow1: darkMode ? '#000' : '#2F80ED', 66 | 67 | // other 68 | red1: '#FF6871', 69 | green1: '#27AE60', 70 | yellow1: '#FFE270', 71 | yellow2: '#F3841E', 72 | link: '#2172E5', 73 | blue: '2f80ed', 74 | 75 | background: darkMode ? 'black' : `radial-gradient(50% 50% at 50% 50%, #ff007a30 0%, #fff 0%)`, 76 | }) 77 | 78 | const TextWrapper = styled(Text)` 79 | color: ${({ color, theme }) => theme[color]}; 80 | ` 81 | 82 | export const TYPE = { 83 | main(props) { 84 | return 85 | }, 86 | 87 | body(props) { 88 | return 89 | }, 90 | 91 | small(props) { 92 | return 93 | }, 94 | 95 | header(props) { 96 | return 97 | }, 98 | 99 | largeHeader(props) { 100 | return 101 | }, 102 | 103 | light(props) { 104 | return 105 | }, 106 | 107 | pink(props) { 108 | return 109 | }, 110 | } 111 | 112 | export const Hover = styled.div` 113 | :hover { 114 | cursor: pointer; 115 | } 116 | ` 117 | 118 | export const Link = styled.a.attrs({ 119 | target: '_blank', 120 | rel: 'noopener noreferrer', 121 | })` 122 | text-decoration: none; 123 | cursor: pointer; 124 | color: ${({ theme }) => theme.primary1}; 125 | font-weight: 500; 126 | :hover { 127 | text-decoration: underline; 128 | } 129 | :focus { 130 | outline: none; 131 | text-decoration: underline; 132 | } 133 | :active { 134 | text-decoration: none; 135 | } 136 | ` 137 | 138 | export const ThemedBackground = styled.div` 139 | position: absolute; 140 | top: 0; 141 | left: 0; 142 | right: 0; 143 | pointer-events: none; 144 | max-width: 100vw !important; 145 | height: 200vh; 146 | mix-blend-mode: color; 147 | background: ${({ backgroundColor }) => 148 | `radial-gradient(50% 50% at 50% 50%, ${backgroundColor} 0%, rgba(255, 255, 255, 0) 100%)`}; 149 | position: absolute; 150 | top: 0px; 151 | left: 0px; 152 | /* z-index: ; */ 153 | 154 | transform: translateY(-110vh); 155 | ` 156 | 157 | export const GlobalStyle = createGlobalStyle` 158 | @import url('https://rsms.me/inter/inter.css'); 159 | html { font-family: 'Inter', sans-serif; } 160 | @supports (font-variation-settings: normal) { 161 | html { font-family: 'Inter var', sans-serif; } 162 | } 163 | 164 | html, 165 | body { 166 | margin: 0; 167 | padding: 0; 168 | width: 100%; 169 | height: 100%; 170 | font-size: 14px; 171 | background-color: ${({ theme }) => theme.bg6}; 172 | } 173 | 174 | a { 175 | text-decoration: none; 176 | 177 | :hover { 178 | text-decoration: none 179 | } 180 | } 181 | 182 | 183 | .three-line-legend { 184 | width: 100%; 185 | height: 70px; 186 | position: absolute; 187 | padding: 8px; 188 | font-size: 12px; 189 | color: #20262E; 190 | background-color: rgba(255, 255, 255, 0.23); 191 | text-align: left; 192 | z-index: 10; 193 | pointer-events: none; 194 | } 195 | 196 | .three-line-legend-dark { 197 | width: 100%; 198 | height: 70px; 199 | position: absolute; 200 | padding: 8px; 201 | font-size: 12px; 202 | color: white; 203 | background-color: rgba(255, 255, 255, 0.23); 204 | text-align: left; 205 | z-index: 10; 206 | pointer-events: none; 207 | } 208 | 209 | @media screen and (max-width: 800px) { 210 | .three-line-legend { 211 | display: none !important; 212 | } 213 | } 214 | 215 | .tv-lightweight-charts{ 216 | width: 100% !important; 217 | 218 | 219 | & > * { 220 | width: 100% !important; 221 | } 222 | } 223 | 224 | 225 | html { 226 | font-size: 1rem; 227 | font-variant: none; 228 | color: 'black'; 229 | -webkit-font-smoothing: antialiased; 230 | -moz-osx-font-smoothing: grayscale; 231 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 232 | height: 100%; 233 | } 234 | ` 235 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/unicorn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/logo_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/CandleChart/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react' 2 | import { createChart, CrosshairMode } from 'lightweight-charts' 3 | import dayjs from 'dayjs' 4 | import { formattedNum } from '../../utils' 5 | import { usePrevious } from 'react-use' 6 | import styled from 'styled-components' 7 | import { Play } from 'react-feather' 8 | import { useDarkModeManager } from '../../contexts/LocalStorage' 9 | 10 | const IconWrapper = styled.div` 11 | position: absolute; 12 | right: 10px; 13 | color: ${({ theme }) => theme.text1} 14 | border-radius: 3px; 15 | height: 16px; 16 | width: 16px; 17 | padding: 0px; 18 | bottom: 10px; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | :hover { 23 | cursor: pointer; 24 | opacity: 0.7; 25 | } 26 | ` 27 | 28 | const CandleStickChart = ({ 29 | data, 30 | width, 31 | height = 300, 32 | base, 33 | margin = true, 34 | valueFormatter = (val) => formattedNum(val, true), 35 | }) => { 36 | // reference for DOM element to create with chart 37 | const ref = useRef() 38 | 39 | const formattedData = data?.map((entry) => { 40 | return { 41 | time: parseFloat(entry.timestamp), 42 | open: parseFloat(entry.open), 43 | low: parseFloat(entry.open), 44 | close: parseFloat(entry.close), 45 | high: parseFloat(entry.close), 46 | } 47 | }) 48 | 49 | if (formattedData && formattedData.length > 0) { 50 | formattedData.push({ 51 | time: dayjs().unix(), 52 | open: parseFloat(formattedData[formattedData.length - 1].close), 53 | close: parseFloat(base), 54 | low: Math.min(parseFloat(base), parseFloat(formattedData[formattedData.length - 1].close)), 55 | high: Math.max(parseFloat(base), parseFloat(formattedData[formattedData.length - 1].close)), 56 | }) 57 | } 58 | 59 | // pointer to the chart object 60 | const [chartCreated, setChartCreated] = useState(false) 61 | const dataPrev = usePrevious(data) 62 | 63 | const [darkMode] = useDarkModeManager() 64 | const textColor = darkMode ? 'white' : 'black' 65 | const previousTheme = usePrevious(darkMode) 66 | 67 | // reset the chart if theme switches 68 | useEffect(() => { 69 | if (chartCreated && previousTheme !== darkMode) { 70 | // remove the tooltip element 71 | let tooltip = document.getElementById('tooltip-id') 72 | let node = document.getElementById('test-id') 73 | node.removeChild(tooltip) 74 | chartCreated.resize(0, 0) 75 | setChartCreated() 76 | } 77 | }, [chartCreated, darkMode, previousTheme]) 78 | 79 | useEffect(() => { 80 | if (data !== dataPrev && chartCreated) { 81 | // remove the tooltip element 82 | let tooltip = document.getElementById('tooltip-id') 83 | let node = document.getElementById('test-id') 84 | node.removeChild(tooltip) 85 | chartCreated.resize(0, 0) 86 | setChartCreated() 87 | } 88 | }, [chartCreated, data, dataPrev]) 89 | 90 | // if no chart created yet, create one with options and add to DOM manually 91 | useEffect(() => { 92 | if (!chartCreated) { 93 | const chart = createChart(ref.current, { 94 | width: width, 95 | height: height, 96 | layout: { 97 | backgroundColor: 'transparent', 98 | textColor: textColor, 99 | }, 100 | grid: { 101 | vertLines: { 102 | color: 'rgba(197, 203, 206, 0.5)', 103 | }, 104 | horzLines: { 105 | color: 'rgba(197, 203, 206, 0.5)', 106 | }, 107 | }, 108 | crosshair: { 109 | mode: CrosshairMode.Normal, 110 | }, 111 | rightPriceScale: { 112 | borderColor: 'rgba(197, 203, 206, 0.8)', 113 | visible: true, 114 | }, 115 | timeScale: { 116 | borderColor: 'rgba(197, 203, 206, 0.8)', 117 | }, 118 | localization: { 119 | priceFormatter: (val) => formattedNum(val), 120 | }, 121 | }) 122 | 123 | var candleSeries = chart.addCandlestickSeries({ 124 | upColor: 'green', 125 | downColor: 'red', 126 | borderDownColor: 'red', 127 | borderUpColor: 'green', 128 | wickDownColor: 'red', 129 | wickUpColor: 'green', 130 | }) 131 | 132 | candleSeries.setData(formattedData) 133 | 134 | var toolTip = document.createElement('div') 135 | toolTip.setAttribute('id', 'tooltip-id') 136 | toolTip.className = 'three-line-legend' 137 | ref.current.appendChild(toolTip) 138 | toolTip.style.display = 'block' 139 | toolTip.style.left = (margin ? 116 : 10) + 'px' 140 | toolTip.style.top = 50 + 'px' 141 | toolTip.style.backgroundColor = 'transparent' 142 | 143 | // get the title of the chart 144 | function setLastBarText() { 145 | toolTip.innerHTML = base 146 | ? `
` + valueFormatter(base) + '
' 147 | : '' 148 | } 149 | setLastBarText() 150 | 151 | // update the title when hovering on the chart 152 | chart.subscribeCrosshairMove(function (param) { 153 | if ( 154 | param === undefined || 155 | param.time === undefined || 156 | param.point.x < 0 || 157 | param.point.x > width || 158 | param.point.y < 0 || 159 | param.point.y > height 160 | ) { 161 | setLastBarText() 162 | } else { 163 | var price = param.seriesPrices.get(candleSeries).close 164 | const time = dayjs.unix(param.time).format('MM/DD h:mm A') 165 | toolTip.innerHTML = 166 | `
` + 167 | valueFormatter(price) + 168 | `` + 169 | time + 170 | ' UTC' + 171 | '' + 172 | '
' 173 | } 174 | }) 175 | 176 | chart.timeScale().fitContent() 177 | 178 | setChartCreated(chart) 179 | } 180 | }, [chartCreated, formattedData, width, height, valueFormatter, base, margin, textColor]) 181 | 182 | // responsiveness 183 | useEffect(() => { 184 | if (width) { 185 | chartCreated && chartCreated.resize(width, height) 186 | chartCreated && chartCreated.timeScale().scrollToPosition(0) 187 | } 188 | }, [chartCreated, height, width]) 189 | 190 | return ( 191 |
192 |
193 | 194 | { 196 | chartCreated && chartCreated.timeScale().fitContent() 197 | }} 198 | /> 199 | 200 |
201 | ) 202 | } 203 | 204 | export default CandleStickChart 205 | -------------------------------------------------------------------------------- /src/components/SideNav/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { AutoColumn } from '../Column' 4 | import Title from '../Title' 5 | import { BasicLink } from '../Link' 6 | import { useMedia } from 'react-use' 7 | import { transparentize } from 'polished' 8 | import { TYPE } from '../../Theme' 9 | import { withRouter } from 'react-router-dom' 10 | import { TrendingUp, List, PieChart, Disc } from 'react-feather' 11 | import Link from '../Link' 12 | import { useSessionStart } from '../../contexts/Application' 13 | import { useDarkModeManager } from '../../contexts/LocalStorage' 14 | import Toggle from '../Toggle' 15 | 16 | const Wrapper = styled.div` 17 | height: ${({ isMobile }) => (isMobile ? 'initial' : '100vh')}; 18 | background-color: ${({ theme }) => transparentize(0.4, theme.bg1)}; 19 | color: ${({ theme }) => theme.text1}; 20 | padding: 0.5rem 0.5rem 0.5rem 0.75rem; 21 | position: sticky; 22 | top: 0px; 23 | z-index: 9999; 24 | box-sizing: border-box; 25 | /* background-color: #1b1c22; */ 26 | background: linear-gradient(193.68deg, #1b1c22 0.68%, #000000 100.48%); 27 | color: ${({ theme }) => theme.bg2}; 28 | 29 | @media screen and (max-width: 800px) { 30 | grid-template-columns: 1fr; 31 | position: relative; 32 | } 33 | 34 | @media screen and (max-width: 600px) { 35 | padding: 1rem; 36 | } 37 | ` 38 | 39 | const Option = styled.div` 40 | font-weight: 500; 41 | font-size: 14px; 42 | opacity: ${({ activeText }) => (activeText ? 1 : 0.6)}; 43 | color: ${({ theme }) => theme.white}; 44 | display: flex; 45 | :hover { 46 | opacity: 1; 47 | } 48 | ` 49 | 50 | const DesktopWrapper = styled.div` 51 | display: flex; 52 | flex-direction: column; 53 | justify-content: space-between; 54 | height: 100vh; 55 | ` 56 | 57 | const MobileWrapper = styled.div` 58 | display: flex; 59 | justify-content: space-between; 60 | align-items: center; 61 | ` 62 | 63 | const HeaderText = styled.div` 64 | margin-right: 0.75rem; 65 | font-size: 0.825rem; 66 | font-weight: 500; 67 | display: inline-box; 68 | display: -webkit-inline-box; 69 | opacity: 0.8; 70 | :hover { 71 | opacity: 1; 72 | } 73 | a { 74 | color: ${({ theme }) => theme.white}; 75 | } 76 | ` 77 | 78 | const Polling = styled.div` 79 | position: fixed; 80 | display: flex; 81 | left: 0; 82 | bottom: 0; 83 | padding: 1rem; 84 | color: white; 85 | opacity: 0.4; 86 | transition: opacity 0.25s ease; 87 | :hover { 88 | opacity: 1; 89 | } 90 | ` 91 | const PollingDot = styled.div` 92 | width: 8px; 93 | height: 8px; 94 | min-height: 8px; 95 | min-width: 8px; 96 | margin-right: 0.5rem; 97 | margin-top: 3px; 98 | border-radius: 50%; 99 | background-color: ${({ theme }) => theme.green1}; 100 | ` 101 | 102 | function SideNav({ history }) { 103 | const below1080 = useMedia('(max-width: 1080px)') 104 | 105 | const below1180 = useMedia('(max-width: 1180px)') 106 | 107 | const seconds = useSessionStart() 108 | 109 | const [isDark, toggleDarkMode] = useDarkModeManager() 110 | 111 | return ( 112 | 113 | {!below1080 ? ( 114 | 115 | 116 | 117 | {!below1080 && ( 118 | <AutoColumn gap="1.25rem" style={{ marginTop: '1rem' }}> 119 | <BasicLink to="/home"> 120 | <Option activeText={history.location.pathname === '/home' ?? undefined}> 121 | <TrendingUp size={20} style={{ marginRight: '.75rem' }} /> 122 | Overview 123 | </Option> 124 | </BasicLink> 125 | <BasicLink to="/tokens"> 126 | <Option 127 | activeText={ 128 | (history.location.pathname.split('/')[1] === 'tokens' || 129 | history.location.pathname.split('/')[1] === 'token') ?? 130 | undefined 131 | } 132 | > 133 | <Disc size={20} style={{ marginRight: '.75rem' }} /> 134 | Tokens 135 | </Option> 136 | </BasicLink> 137 | <BasicLink to="/pairs"> 138 | <Option 139 | activeText={ 140 | (history.location.pathname.split('/')[1] === 'pairs' || 141 | history.location.pathname.split('/')[1] === 'pair') ?? 142 | undefined 143 | } 144 | > 145 | <PieChart size={20} style={{ marginRight: '.75rem' }} /> 146 | Pairs 147 | </Option> 148 | </BasicLink> 149 | 150 | <BasicLink to="/accounts"> 151 | <Option 152 | activeText={ 153 | (history.location.pathname.split('/')[1] === 'accounts' || 154 | history.location.pathname.split('/')[1] === 'account') ?? 155 | undefined 156 | } 157 | > 158 | <List size={20} style={{ marginRight: '.75rem' }} /> 159 | Accounts 160 | </Option> 161 | </BasicLink> 162 | </AutoColumn> 163 | )} 164 | </AutoColumn> 165 | <AutoColumn gap="0.5rem" style={{ marginLeft: '.75rem', marginBottom: '4rem' }}> 166 | <HeaderText> 167 | <Link href="https://uniswap.org" target="_blank"> 168 | Uniswap.org 169 | </Link> 170 | </HeaderText> 171 | <HeaderText> 172 | <Link href="https://v1.uniswap.info" target="_blank"> 173 | V1 Analytics 174 | </Link> 175 | </HeaderText> 176 | <HeaderText> 177 | <Link href="https://uniswap.org/docs/v2" target="_blank"> 178 | Docs 179 | </Link> 180 | </HeaderText> 181 | <HeaderText> 182 | <Link href="https://discord.com/invite/FCfyBSbCU5" target="_blank"> 183 | Discord 184 | </Link> 185 | </HeaderText> 186 | <HeaderText> 187 | <Link href="https://twitter.com/UniswapProtocol" target="_blank"> 188 | Twitter 189 | </Link> 190 | </HeaderText> 191 | <Toggle isActive={isDark} toggle={toggleDarkMode} /> 192 | </AutoColumn> 193 | {!below1180 && ( 194 | <Polling style={{ marginLeft: '.5rem' }}> 195 | <PollingDot /> 196 | <a href="/" style={{ color: 'white' }}> 197 | <TYPE.small color={'white'}> 198 | Updated {!!seconds ? seconds + 's' : '-'} ago <br /> 199 | </TYPE.small> 200 | </a> 201 | </Polling> 202 | )} 203 | </DesktopWrapper> 204 | ) : ( 205 | <MobileWrapper> 206 | <Title /> 207 | </MobileWrapper> 208 | )} 209 | </Wrapper> 210 | ) 211 | } 212 | 213 | export default withRouter(SideNav) 214 | -------------------------------------------------------------------------------- /src/pages/GlobalPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { withRouter } from 'react-router-dom' 3 | import { Box } from 'rebass' 4 | import styled from 'styled-components' 5 | 6 | import { AutoRow, RowBetween } from '../components/Row' 7 | import { AutoColumn } from '../components/Column' 8 | import PairList from '../components/PairList' 9 | import TopTokenList from '../components/TokenList' 10 | import TxnList from '../components/TxnList' 11 | import GlobalChart from '../components/GlobalChart' 12 | import Search from '../components/Search' 13 | import GlobalStats from '../components/GlobalStats' 14 | 15 | import { useGlobalData, useGlobalTransactions } from '../contexts/GlobalData' 16 | import { useAllPairData } from '../contexts/PairData' 17 | import { useMedia } from 'react-use' 18 | import Panel from '../components/Panel' 19 | import { useAllTokenData } from '../contexts/TokenData' 20 | import { formattedNum, formattedPercent } from '../utils' 21 | import { TYPE, ThemedBackground } from '../Theme' 22 | import { transparentize } from 'polished' 23 | import { CustomLink } from '../components/Link' 24 | 25 | import { PageWrapper, ContentWrapper } from '../components' 26 | import CheckBox from '../components/Checkbox' 27 | import QuestionHelper from '../components/QuestionHelper' 28 | 29 | const ListOptions = styled(AutoRow)` 30 | height: 40px; 31 | width: 100%; 32 | font-size: 1.25rem; 33 | font-weight: 600; 34 | 35 | @media screen and (max-width: 640px) { 36 | font-size: 1rem; 37 | } 38 | ` 39 | 40 | const GridRow = styled.div` 41 | display: grid; 42 | width: 100%; 43 | grid-template-columns: 1fr 1fr; 44 | column-gap: 6px; 45 | align-items: start; 46 | justify-content: space-between; 47 | ` 48 | 49 | function GlobalPage() { 50 | // get data for lists and totals 51 | const allPairs = useAllPairData() 52 | const allTokens = useAllTokenData() 53 | const transactions = useGlobalTransactions() 54 | const { totalLiquidityUSD, oneDayVolumeUSD, volumeChangeUSD, liquidityChangeUSD } = useGlobalData() 55 | 56 | // breakpoints 57 | const below800 = useMedia('(max-width: 800px)') 58 | 59 | // scrolling refs 60 | useEffect(() => { 61 | document.querySelector('body').scrollTo({ 62 | behavior: 'smooth', 63 | top: 0, 64 | }) 65 | }, []) 66 | 67 | // for tracked data on pairs 68 | const [useTracked, setUseTracked] = useState(true) 69 | 70 | return ( 71 | <PageWrapper> 72 | <ThemedBackground backgroundColor={transparentize(0.6, '#ff007a')} /> 73 | <ContentWrapper> 74 | <div> 75 | <AutoColumn gap="24px" style={{ paddingBottom: below800 ? '0' : '24px' }}> 76 | <TYPE.largeHeader>{below800 ? 'Uniswap Analytics' : 'Uniswap Analytics'}</TYPE.largeHeader> 77 | <Search /> 78 | <GlobalStats /> 79 | </AutoColumn> 80 | {below800 && ( // mobile card 81 | <Box mb={20}> 82 | <Panel> 83 | <Box> 84 | <AutoColumn gap="36px"> 85 | <AutoColumn gap="20px"> 86 | <RowBetween> 87 | <TYPE.main>Volume (24hrs)</TYPE.main> 88 | <div /> 89 | </RowBetween> 90 | <RowBetween align="flex-end"> 91 | <TYPE.main fontSize={'1.5rem'} lineHeight={1} fontWeight={600}> 92 | {oneDayVolumeUSD ? formattedNum(oneDayVolumeUSD, true) : '-'} 93 | </TYPE.main> 94 | <TYPE.main fontSize={12}>{volumeChangeUSD ? formattedPercent(volumeChangeUSD) : '-'}</TYPE.main> 95 | </RowBetween> 96 | </AutoColumn> 97 | <AutoColumn gap="20px"> 98 | <RowBetween> 99 | <TYPE.main>Total Liquidity</TYPE.main> 100 | <div /> 101 | </RowBetween> 102 | <RowBetween align="flex-end"> 103 | <TYPE.main fontSize={'1.5rem'} lineHeight={1} fontWeight={600}> 104 | {totalLiquidityUSD ? formattedNum(totalLiquidityUSD, true) : '-'} 105 | </TYPE.main> 106 | <TYPE.main fontSize={12}> 107 | {liquidityChangeUSD ? formattedPercent(liquidityChangeUSD) : '-'} 108 | </TYPE.main> 109 | </RowBetween> 110 | </AutoColumn> 111 | </AutoColumn> 112 | </Box> 113 | </Panel> 114 | </Box> 115 | )} 116 | {!below800 && ( 117 | <GridRow> 118 | <Panel style={{ height: '100%', minHeight: '300px' }}> 119 | <GlobalChart display="liquidity" /> 120 | </Panel> 121 | <Panel style={{ height: '100%' }}> 122 | <GlobalChart display="volume" /> 123 | </Panel> 124 | </GridRow> 125 | )} 126 | {below800 && ( 127 | <AutoColumn style={{ marginTop: '6px' }} gap="24px"> 128 | <Panel style={{ height: '100%', minHeight: '300px' }}> 129 | <GlobalChart display="liquidity" /> 130 | </Panel> 131 | </AutoColumn> 132 | )} 133 | <ListOptions gap="10px" style={{ marginTop: '2rem', marginBottom: '.5rem' }}> 134 | <RowBetween> 135 | <TYPE.main fontSize={'1.125rem'} style={{ whiteSpace: 'nowrap' }}> 136 | Top Tokens 137 | </TYPE.main> 138 | <CustomLink to={'/tokens'}>See All</CustomLink> 139 | </RowBetween> 140 | </ListOptions> 141 | <Panel style={{ marginTop: '6px', padding: '1.125rem 0 ' }}> 142 | <TopTokenList tokens={allTokens} /> 143 | </Panel> 144 | <ListOptions gap="10px" style={{ marginTop: '2rem', marginBottom: '.5rem' }}> 145 | <RowBetween> 146 | <TYPE.main fontSize={'1rem'} style={{ whiteSpace: 'nowrap' }}> 147 | Top Pairs 148 | </TYPE.main> 149 | <AutoRow gap="4px" width="100%" justifyContent="flex-end"> 150 | <CheckBox 151 | checked={useTracked} 152 | setChecked={() => setUseTracked(!useTracked)} 153 | text={'Hide untracked pairs'} 154 | /> 155 | <QuestionHelper text="USD amounts may be inaccurate in low liquiidty pairs or pairs without ETH or stablecoins." /> 156 | <CustomLink to={'/pairs'}>See All</CustomLink> 157 | </AutoRow> 158 | </RowBetween> 159 | </ListOptions> 160 | <Panel style={{ marginTop: '6px', padding: '1.125rem 0 ' }}> 161 | <PairList pairs={allPairs} useTracked={useTracked} /> 162 | </Panel> 163 | <span> 164 | <TYPE.main fontSize={'1.125rem'} style={{ marginTop: '2rem' }}> 165 | Transactions 166 | </TYPE.main> 167 | </span> 168 | <Panel style={{ margin: '1rem 0' }}> 169 | <TxnList transactions={transactions} /> 170 | </Panel> 171 | </div> 172 | </ContentWrapper> 173 | </PageWrapper> 174 | ) 175 | } 176 | 177 | export default withRouter(GlobalPage) 178 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from 'styled-components' 3 | import { ApolloProvider } from 'react-apollo' 4 | import { client } from './apollo/client' 5 | import { Route, Switch, BrowserRouter, Redirect } from 'react-router-dom' 6 | import GlobalPage from './pages/GlobalPage' 7 | import TokenPage from './pages/TokenPage' 8 | import PairPage from './pages/PairPage' 9 | import { useGlobalData, useGlobalChartData } from './contexts/GlobalData' 10 | import { isAddress } from './utils' 11 | import AccountPage from './pages/AccountPage' 12 | import AllTokensPage from './pages/AllTokensPage' 13 | import AllPairsPage from './pages/AllPairsPage' 14 | import PinnedData from './components/PinnedData' 15 | 16 | import SideNav from './components/SideNav' 17 | import AccountLookup from './pages/AccountLookup' 18 | import LocalLoader from './components/LocalLoader' 19 | import { useLatestBlocks } from './contexts/Application' 20 | import GoogleAnalyticsReporter from './components/analytics/GoogleAnalyticsReporter' 21 | import { PAIR_BLACKLIST, TOKEN_BLACKLIST } from './constants' 22 | 23 | const AppWrapper = styled.div` 24 | position: relative; 25 | width: 100%; 26 | ` 27 | const ContentWrapper = styled.div` 28 | display: grid; 29 | grid-template-columns: ${({ open }) => (open ? '220px 1fr 200px' : '220px 1fr 64px')}; 30 | 31 | @media screen and (max-width: 1400px) { 32 | grid-template-columns: 220px 1fr; 33 | } 34 | 35 | @media screen and (max-width: 1080px) { 36 | grid-template-columns: 1fr; 37 | max-width: 100vw; 38 | overflow: hidden; 39 | grid-gap: 0; 40 | } 41 | ` 42 | 43 | const Right = styled.div` 44 | position: fixed; 45 | right: 0; 46 | bottom: 0rem; 47 | z-index: 99; 48 | width: ${({ open }) => (open ? '220px' : '64px')}; 49 | height: ${({ open }) => (open ? 'fit-content' : '64px')}; 50 | overflow: auto; 51 | background-color: ${({ theme }) => theme.bg1}; 52 | @media screen and (max-width: 1400px) { 53 | display: none; 54 | } 55 | ` 56 | 57 | const Center = styled.div` 58 | height: 100%; 59 | z-index: 9999; 60 | transition: width 0.25s ease; 61 | background-color: ${({ theme }) => theme.onlyLight}; 62 | ` 63 | 64 | const WarningWrapper = styled.div` 65 | width: 100%; 66 | display: flex; 67 | justify-content: center; 68 | ` 69 | 70 | const WarningBanner = styled.div` 71 | background-color: #ff6871; 72 | padding: 1.5rem; 73 | color: white; 74 | width: 100%; 75 | text-align: center; 76 | font-weight: 500; 77 | ` 78 | 79 | /** 80 | * Wrap the component with the header and sidebar pinned tab 81 | */ 82 | const LayoutWrapper = ({ children, savedOpen, setSavedOpen }) => { 83 | return ( 84 | <> 85 | <ContentWrapper open={savedOpen}> 86 | <SideNav /> 87 | <Center id="center">{children}</Center> 88 | <Right open={savedOpen}> 89 | <PinnedData open={savedOpen} setSavedOpen={setSavedOpen} /> 90 | </Right> 91 | </ContentWrapper> 92 | </> 93 | ) 94 | } 95 | 96 | const BLOCK_DIFFERENCE_THRESHOLD = 30 97 | 98 | function App() { 99 | const [savedOpen, setSavedOpen] = useState(false) 100 | 101 | const globalData = useGlobalData() 102 | const globalChartData = useGlobalChartData() 103 | const [latestBlock, headBlock] = useLatestBlocks() 104 | 105 | // show warning 106 | const showWarning = headBlock && latestBlock ? headBlock - latestBlock > BLOCK_DIFFERENCE_THRESHOLD : false 107 | 108 | return ( 109 | <ApolloProvider client={client}> 110 | <AppWrapper> 111 | {showWarning && ( 112 | <WarningWrapper> 113 | <WarningBanner> 114 | {`Warning: The data on this site has only synced to Ethereum block ${latestBlock} (out of ${headBlock}). Please check back soon.`} 115 | </WarningBanner> 116 | </WarningWrapper> 117 | )} 118 | {globalData && 119 | Object.keys(globalData).length > 0 && 120 | globalChartData && 121 | Object.keys(globalChartData).length > 0 ? ( 122 | <BrowserRouter> 123 | <Route component={GoogleAnalyticsReporter} /> 124 | <Switch> 125 | <Route 126 | exacts 127 | strict 128 | path="/token/:tokenAddress" 129 | render={({ match }) => { 130 | if ( 131 | isAddress(match.params.tokenAddress.toLowerCase()) && 132 | !Object.keys(TOKEN_BLACKLIST).includes(match.params.tokenAddress.toLowerCase()) 133 | ) { 134 | return ( 135 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 136 | <TokenPage address={match.params.tokenAddress.toLowerCase()} /> 137 | </LayoutWrapper> 138 | ) 139 | } else { 140 | return <Redirect to="/home" /> 141 | } 142 | }} 143 | /> 144 | <Route 145 | exacts 146 | strict 147 | path="/pair/:pairAddress" 148 | render={({ match }) => { 149 | if ( 150 | isAddress(match.params.pairAddress.toLowerCase()) && 151 | !Object.keys(PAIR_BLACKLIST).includes(match.params.pairAddress.toLowerCase()) 152 | ) { 153 | return ( 154 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 155 | <PairPage pairAddress={match.params.pairAddress.toLowerCase()} /> 156 | </LayoutWrapper> 157 | ) 158 | } else { 159 | return <Redirect to="/home" /> 160 | } 161 | }} 162 | /> 163 | <Route 164 | exacts 165 | strict 166 | path="/account/:accountAddress" 167 | render={({ match }) => { 168 | if (isAddress(match.params.accountAddress.toLowerCase())) { 169 | return ( 170 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 171 | <AccountPage account={match.params.accountAddress.toLowerCase()} /> 172 | </LayoutWrapper> 173 | ) 174 | } else { 175 | return <Redirect to="/home" /> 176 | } 177 | }} 178 | /> 179 | 180 | <Route path="/home"> 181 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 182 | <GlobalPage /> 183 | </LayoutWrapper> 184 | </Route> 185 | 186 | <Route path="/tokens"> 187 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 188 | <AllTokensPage /> 189 | </LayoutWrapper> 190 | </Route> 191 | 192 | <Route path="/pairs"> 193 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 194 | <AllPairsPage /> 195 | </LayoutWrapper> 196 | </Route> 197 | 198 | <Route path="/accounts"> 199 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 200 | <AccountLookup /> 201 | </LayoutWrapper> 202 | </Route> 203 | 204 | <Redirect to="/home" /> 205 | </Switch> 206 | </BrowserRouter> 207 | ) : ( 208 | <LocalLoader fill="true" /> 209 | )} 210 | </AppWrapper> 211 | </ApolloProvider> 212 | ) 213 | } 214 | 215 | export default App 216 | -------------------------------------------------------------------------------- /src/components/TradingviewChart/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react' 2 | import { createChart } from 'lightweight-charts' 3 | import dayjs from 'dayjs' 4 | import utc from 'dayjs/plugin/utc' 5 | import { formattedNum } from '../../utils' 6 | import styled from 'styled-components' 7 | import { usePrevious } from 'react-use' 8 | import { Play } from 'react-feather' 9 | import { useDarkModeManager } from '../../contexts/LocalStorage' 10 | import { IconWrapper } from '..' 11 | 12 | dayjs.extend(utc) 13 | 14 | export const CHART_TYPES = { 15 | BAR: 'BAR', 16 | AREA: 'AREA', 17 | } 18 | 19 | const Wrapper = styled.div` 20 | position: relative; 21 | ` 22 | 23 | // constant height for charts 24 | const HEIGHT = 300 25 | 26 | const TradingViewChart = ({ 27 | type = CHART_TYPES.BAR, 28 | data, 29 | base, 30 | baseChange, 31 | field, 32 | title, 33 | width, 34 | useWeekly = false, 35 | }) => { 36 | // reference for DOM element to create with chart 37 | const ref = useRef() 38 | 39 | // pointer to the chart object 40 | const [chartCreated, setChartCreated] = useState(false) 41 | const dataPrev = usePrevious(data) 42 | 43 | useEffect(() => { 44 | if (data !== dataPrev && chartCreated && type === CHART_TYPES.BAR) { 45 | // remove the tooltip element 46 | let tooltip = document.getElementById('tooltip-id' + type) 47 | let node = document.getElementById('test-id' + type) 48 | node.removeChild(tooltip) 49 | chartCreated.resize(0, 0) 50 | setChartCreated() 51 | } 52 | }, [chartCreated, data, dataPrev, type]) 53 | 54 | // parese the data and format for tardingview consumption 55 | const formattedData = data?.map((entry) => { 56 | return { 57 | time: dayjs.unix(entry.date).utc().format('YYYY-MM-DD'), 58 | value: parseFloat(entry[field]), 59 | } 60 | }) 61 | 62 | // adjust the scale based on the type of chart 63 | const topScale = type === CHART_TYPES.AREA ? 0.32 : 0.2 64 | 65 | const [darkMode] = useDarkModeManager() 66 | const textColor = darkMode ? 'white' : 'black' 67 | const previousTheme = usePrevious(darkMode) 68 | 69 | // reset the chart if them switches 70 | useEffect(() => { 71 | if (chartCreated && previousTheme !== darkMode) { 72 | // remove the tooltip element 73 | let tooltip = document.getElementById('tooltip-id' + type) 74 | let node = document.getElementById('test-id' + type) 75 | node.removeChild(tooltip) 76 | chartCreated.resize(0, 0) 77 | setChartCreated() 78 | } 79 | }, [chartCreated, darkMode, previousTheme, type]) 80 | 81 | // if no chart created yet, create one with options and add to DOM manually 82 | useEffect(() => { 83 | if (!chartCreated && formattedData) { 84 | var chart = createChart(ref.current, { 85 | width: width, 86 | height: HEIGHT, 87 | layout: { 88 | backgroundColor: 'transparent', 89 | textColor: textColor, 90 | }, 91 | rightPriceScale: { 92 | scaleMargins: { 93 | top: topScale, 94 | bottom: 0, 95 | }, 96 | borderVisible: false, 97 | }, 98 | timeScale: { 99 | borderVisible: false, 100 | }, 101 | grid: { 102 | horzLines: { 103 | color: 'rgba(197, 203, 206, 0.5)', 104 | visible: false, 105 | }, 106 | vertLines: { 107 | color: 'rgba(197, 203, 206, 0.5)', 108 | visible: false, 109 | }, 110 | }, 111 | crosshair: { 112 | horzLine: { 113 | visible: false, 114 | labelVisible: false, 115 | }, 116 | vertLine: { 117 | visible: true, 118 | style: 0, 119 | width: 2, 120 | color: 'rgba(32, 38, 46, 0.1)', 121 | labelVisible: false, 122 | }, 123 | }, 124 | localization: { 125 | priceFormatter: (val) => formattedNum(val, true), 126 | }, 127 | }) 128 | 129 | var series = 130 | type === CHART_TYPES.BAR 131 | ? chart.addHistogramSeries({ 132 | color: '#ff007a', 133 | priceFormat: { 134 | type: 'volume', 135 | }, 136 | scaleMargins: { 137 | top: 0.32, 138 | bottom: 0, 139 | }, 140 | lineColor: '#ff007a', 141 | lineWidth: 3, 142 | }) 143 | : chart.addAreaSeries({ 144 | topColor: '#ff007a', 145 | bottomColor: 'rgba(255, 0, 122, 0)', 146 | lineColor: '#ff007a', 147 | lineWidth: 3, 148 | }) 149 | 150 | series.setData(formattedData) 151 | var toolTip = document.createElement('div') 152 | toolTip.setAttribute('id', 'tooltip-id' + type) 153 | toolTip.className = darkMode ? 'three-line-legend-dark' : 'three-line-legend' 154 | ref.current.appendChild(toolTip) 155 | toolTip.style.display = 'block' 156 | toolTip.style.fontWeight = '500' 157 | toolTip.style.left = -4 + 'px' 158 | toolTip.style.top = '-' + 8 + 'px' 159 | toolTip.style.backgroundColor = 'transparent' 160 | 161 | // format numbers 162 | let percentChange = baseChange?.toFixed(2) 163 | let formattedPercentChange = (percentChange > 0 ? '+' : '') + percentChange + '%' 164 | let color = percentChange >= 0 ? 'green' : 'red' 165 | 166 | // get the title of the chart 167 | function setLastBarText() { 168 | toolTip.innerHTML = 169 | `<div style="font-size: 16px; margin: 4px 0px; color: ${textColor};">${title} ${ 170 | type === CHART_TYPES.BAR && !useWeekly ? '(24hr)' : '' 171 | }</div>` + 172 | `<div style="font-size: 22px; margin: 4px 0px; color:${textColor}" >` + 173 | formattedNum(base ?? 0, true) + 174 | `<span style="margin-left: 10px; font-size: 16px; color: ${color};">${formattedPercentChange}</span>` + 175 | '</div>' 176 | } 177 | setLastBarText() 178 | 179 | // update the title when hovering on the chart 180 | chart.subscribeCrosshairMove(function (param) { 181 | if ( 182 | param === undefined || 183 | param.time === undefined || 184 | param.point.x < 0 || 185 | param.point.x > width || 186 | param.point.y < 0 || 187 | param.point.y > HEIGHT 188 | ) { 189 | setLastBarText() 190 | } else { 191 | let dateStr = useWeekly 192 | ? dayjs(param.time.year + '-' + param.time.month + '-' + param.time.day) 193 | .startOf('week') 194 | .format('MMMM D, YYYY') + 195 | '-' + 196 | dayjs(param.time.year + '-' + param.time.month + '-' + param.time.day) 197 | .endOf('week') 198 | .format('MMMM D, YYYY') 199 | : dayjs(param.time.year + '-' + param.time.month + '-' + param.time.day).format('MMMM D, YYYY') 200 | var price = param.seriesPrices.get(series) 201 | 202 | toolTip.innerHTML = 203 | `<div style="font-size: 16px; margin: 4px 0px; color: ${textColor};">${title}</div>` + 204 | `<div style="font-size: 22px; margin: 4px 0px; color: ${textColor}">` + 205 | formattedNum(price, true) + 206 | '</div>' + 207 | '<div>' + 208 | dateStr + 209 | '</div>' 210 | } 211 | }) 212 | 213 | chart.timeScale().fitContent() 214 | 215 | setChartCreated(chart) 216 | } 217 | }, [ 218 | base, 219 | baseChange, 220 | chartCreated, 221 | darkMode, 222 | data, 223 | formattedData, 224 | textColor, 225 | title, 226 | topScale, 227 | type, 228 | useWeekly, 229 | width, 230 | ]) 231 | 232 | // responsiveness 233 | useEffect(() => { 234 | if (width) { 235 | chartCreated && chartCreated.resize(width, HEIGHT) 236 | chartCreated && chartCreated.timeScale().scrollToPosition(0) 237 | } 238 | }, [chartCreated, width]) 239 | 240 | return ( 241 | <Wrapper> 242 | <div ref={ref} id={'test-id' + type} /> 243 | <IconWrapper> 244 | <Play 245 | onClick={() => { 246 | chartCreated && chartCreated.timeScale().fitContent() 247 | }} 248 | /> 249 | </IconWrapper> 250 | </Wrapper> 251 | ) 252 | } 253 | 254 | export default TradingViewChart 255 | -------------------------------------------------------------------------------- /src/contexts/Application.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useReducer, useMemo, useCallback, useState, useEffect } from 'react' 2 | import { timeframeOptions, SUPPORTED_LIST_URLS__NO_ENS } from '../constants' 3 | import dayjs from 'dayjs' 4 | import utc from 'dayjs/plugin/utc' 5 | import getTokenList from '../utils/tokenLists' 6 | import { healthClient } from '../apollo/client' 7 | import { SUBGRAPH_HEALTH } from '../apollo/queries' 8 | dayjs.extend(utc) 9 | 10 | const UPDATE = 'UPDATE' 11 | const UPDATE_TIMEFRAME = 'UPDATE_TIMEFRAME' 12 | const UPDATE_SESSION_START = 'UPDATE_SESSION_START' 13 | const UPDATED_SUPPORTED_TOKENS = 'UPDATED_SUPPORTED_TOKENS' 14 | const UPDATE_LATEST_BLOCK = 'UPDATE_LATEST_BLOCK' 15 | const UPDATE_HEAD_BLOCK = 'UPDATE_HEAD_BLOCK' 16 | 17 | const SUPPORTED_TOKENS = 'SUPPORTED_TOKENS' 18 | const TIME_KEY = 'TIME_KEY' 19 | const CURRENCY = 'CURRENCY' 20 | const SESSION_START = 'SESSION_START' 21 | const LATEST_BLOCK = 'LATEST_BLOCK' 22 | const HEAD_BLOCK = 'HEAD_BLOCK' 23 | 24 | const ApplicationContext = createContext() 25 | 26 | function useApplicationContext() { 27 | return useContext(ApplicationContext) 28 | } 29 | 30 | function reducer(state, { type, payload }) { 31 | switch (type) { 32 | case UPDATE: { 33 | const { currency } = payload 34 | return { 35 | ...state, 36 | [CURRENCY]: currency, 37 | } 38 | } 39 | case UPDATE_TIMEFRAME: { 40 | const { newTimeFrame } = payload 41 | return { 42 | ...state, 43 | [TIME_KEY]: newTimeFrame, 44 | } 45 | } 46 | case UPDATE_SESSION_START: { 47 | const { timestamp } = payload 48 | return { 49 | ...state, 50 | [SESSION_START]: timestamp, 51 | } 52 | } 53 | 54 | case UPDATE_LATEST_BLOCK: { 55 | const { block } = payload 56 | return { 57 | ...state, 58 | [LATEST_BLOCK]: block, 59 | } 60 | } 61 | 62 | case UPDATE_HEAD_BLOCK: { 63 | const { block } = payload 64 | return { 65 | ...state, 66 | [HEAD_BLOCK]: block, 67 | } 68 | } 69 | 70 | case UPDATED_SUPPORTED_TOKENS: { 71 | const { supportedTokens } = payload 72 | return { 73 | ...state, 74 | [SUPPORTED_TOKENS]: supportedTokens, 75 | } 76 | } 77 | 78 | default: { 79 | throw Error(`Unexpected action type in DataContext reducer: '${type}'.`) 80 | } 81 | } 82 | } 83 | 84 | const INITIAL_STATE = { 85 | CURRENCY: 'USD', 86 | TIME_KEY: timeframeOptions.ALL_TIME, 87 | } 88 | 89 | export default function Provider({ children }) { 90 | const [state, dispatch] = useReducer(reducer, INITIAL_STATE) 91 | const update = useCallback((currency) => { 92 | dispatch({ 93 | type: UPDATE, 94 | payload: { 95 | currency, 96 | }, 97 | }) 98 | }, []) 99 | 100 | // global time window for charts - see timeframe options in constants 101 | const updateTimeframe = useCallback((newTimeFrame) => { 102 | dispatch({ 103 | type: UPDATE_TIMEFRAME, 104 | payload: { 105 | newTimeFrame, 106 | }, 107 | }) 108 | }, []) 109 | 110 | // used for refresh button 111 | const updateSessionStart = useCallback((timestamp) => { 112 | dispatch({ 113 | type: UPDATE_SESSION_START, 114 | payload: { 115 | timestamp, 116 | }, 117 | }) 118 | }, []) 119 | 120 | const updateSupportedTokens = useCallback((supportedTokens) => { 121 | dispatch({ 122 | type: UPDATED_SUPPORTED_TOKENS, 123 | payload: { 124 | supportedTokens, 125 | }, 126 | }) 127 | }, []) 128 | 129 | const updateLatestBlock = useCallback((block) => { 130 | dispatch({ 131 | type: UPDATE_LATEST_BLOCK, 132 | payload: { 133 | block, 134 | }, 135 | }) 136 | }, []) 137 | 138 | const updateHeadBlock = useCallback((block) => { 139 | dispatch({ 140 | type: UPDATE_HEAD_BLOCK, 141 | payload: { 142 | block, 143 | }, 144 | }) 145 | }, []) 146 | 147 | return ( 148 | <ApplicationContext.Provider 149 | value={useMemo( 150 | () => [ 151 | state, 152 | { 153 | update, 154 | updateSessionStart, 155 | updateTimeframe, 156 | updateSupportedTokens, 157 | updateLatestBlock, 158 | updateHeadBlock, 159 | }, 160 | ], 161 | [state, update, updateTimeframe, updateSessionStart, updateSupportedTokens, updateLatestBlock, updateHeadBlock] 162 | )} 163 | > 164 | {children} 165 | </ApplicationContext.Provider> 166 | ) 167 | } 168 | 169 | export function useLatestBlocks() { 170 | const [state, { updateLatestBlock, updateHeadBlock }] = useApplicationContext() 171 | 172 | const latestBlock = state?.[LATEST_BLOCK] 173 | const headBlock = state?.[HEAD_BLOCK] 174 | 175 | useEffect(() => { 176 | async function fetch() { 177 | healthClient 178 | .query({ 179 | query: SUBGRAPH_HEALTH, 180 | }) 181 | .then((res) => { 182 | const syncedBlock = res.data.indexingStatusForCurrentVersion.chains[0].latestBlock.number 183 | const headBlock = res.data.indexingStatusForCurrentVersion.chains[0].chainHeadBlock.number 184 | if (syncedBlock && headBlock) { 185 | updateLatestBlock(syncedBlock) 186 | updateHeadBlock(headBlock) 187 | } 188 | }) 189 | .catch((e) => { 190 | console.log(e) 191 | }) 192 | } 193 | if (!latestBlock) { 194 | fetch() 195 | } 196 | }, [latestBlock, updateHeadBlock, updateLatestBlock]) 197 | 198 | return [latestBlock, headBlock] 199 | } 200 | 201 | export function useCurrentCurrency() { 202 | const [state, { update }] = useApplicationContext() 203 | const toggleCurrency = useCallback(() => { 204 | if (state.currency === 'ETH') { 205 | update('USD') 206 | } else { 207 | update('ETH') 208 | } 209 | }, [state, update]) 210 | return [state[CURRENCY], toggleCurrency] 211 | } 212 | 213 | export function useTimeframe() { 214 | const [state, { updateTimeframe }] = useApplicationContext() 215 | const activeTimeframe = state?.[`TIME_KEY`] 216 | return [activeTimeframe, updateTimeframe] 217 | } 218 | 219 | export function useStartTimestamp() { 220 | const [activeWindow] = useTimeframe() 221 | const [startDateTimestamp, setStartDateTimestamp] = useState() 222 | 223 | // monitor the old date fetched 224 | useEffect(() => { 225 | let startTime = 226 | dayjs 227 | .utc() 228 | .subtract( 229 | 1, 230 | activeWindow === timeframeOptions.week ? 'week' : activeWindow === timeframeOptions.ALL_TIME ? 'year' : 'year' 231 | ) 232 | .startOf('day') 233 | .unix() - 1 234 | // if we find a new start time less than the current startrtime - update oldest pooint to fetch 235 | setStartDateTimestamp(startTime) 236 | }, [activeWindow, startDateTimestamp]) 237 | 238 | return startDateTimestamp 239 | } 240 | 241 | // keep track of session length for refresh ticker 242 | export function useSessionStart() { 243 | const [state, { updateSessionStart }] = useApplicationContext() 244 | const sessionStart = state?.[SESSION_START] 245 | 246 | useEffect(() => { 247 | if (!sessionStart) { 248 | updateSessionStart(Date.now()) 249 | } 250 | }) 251 | 252 | const [seconds, setSeconds] = useState(0) 253 | 254 | useEffect(() => { 255 | let interval = null 256 | interval = setInterval(() => { 257 | setSeconds(Date.now() - sessionStart ?? Date.now()) 258 | }, 1000) 259 | 260 | return () => clearInterval(interval) 261 | }, [seconds, sessionStart]) 262 | 263 | return parseInt(seconds / 1000) 264 | } 265 | 266 | export function useListedTokens() { 267 | const [state, { updateSupportedTokens }] = useApplicationContext() 268 | const supportedTokens = state?.[SUPPORTED_TOKENS] 269 | 270 | useEffect(() => { 271 | async function fetchList() { 272 | const allFetched = await SUPPORTED_LIST_URLS__NO_ENS.reduce(async (fetchedTokens, url) => { 273 | const tokensSoFar = await fetchedTokens 274 | const newTokens = await getTokenList(url) 275 | if (newTokens?.tokens) { 276 | return Promise.resolve([...tokensSoFar, ...newTokens.tokens]) 277 | } 278 | }, Promise.resolve([])) 279 | let formatted = allFetched?.map((t) => t.address.toLowerCase()) 280 | updateSupportedTokens(formatted) 281 | } 282 | if (!supportedTokens) { 283 | try { 284 | fetchList() 285 | } catch { 286 | console.log('Error fetching') 287 | } 288 | } 289 | }, [updateSupportedTokens, supportedTokens]) 290 | 291 | return supportedTokens 292 | } 293 | -------------------------------------------------------------------------------- /cloudformation.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "info.uniswap.org site", 4 | "Outputs": { 5 | "DeploymentAccessKeyId": { 6 | "Description": "Access key ID for deploying to the S3 bucket", 7 | "Value": { 8 | "Ref": "DeploymentUserAccessKey" 9 | } 10 | }, 11 | "DeploymentSecretAccessKey": { 12 | "Description": "Secret access key for deploying to the S3 bucket", 13 | "Value": { 14 | "Fn::GetAtt": ["DeploymentUserAccessKey", "SecretAccessKey"] 15 | } 16 | } 17 | }, 18 | "Resources": { 19 | "DeploymentUser": { 20 | "Type": "AWS::IAM::User", 21 | "Description": "The user that will be used to deploy to the assets bucket", 22 | "Properties": { 23 | "Policies": [ 24 | { 25 | "PolicyDocument": { 26 | "Version": "2012-10-17", 27 | "Statement": [ 28 | { 29 | "Sid": "1", 30 | "Effect": "Allow", 31 | "Action": ["s3:PutObject", "s3:GetObject", "s3:ListBucket"], 32 | "Resource": [ 33 | { 34 | "Fn::Join": [ 35 | "", 36 | [ 37 | "arn:aws:s3:::", 38 | { 39 | "Ref": "AssetsBucket" 40 | }, 41 | "/*" 42 | ] 43 | ] 44 | }, 45 | { 46 | "Fn::Join": [ 47 | "", 48 | [ 49 | "arn:aws:s3:::", 50 | { 51 | "Ref": "AssetsBucket" 52 | } 53 | ] 54 | ] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | "PolicyName": "deploy-to-assets-bucket" 61 | } 62 | ] 63 | } 64 | }, 65 | "DeploymentUserAccessKey": { 66 | "Type": "AWS::IAM::AccessKey", 67 | "Description": "Access key for the deployment user", 68 | "Properties": { 69 | "UserName": { 70 | "Ref": "DeploymentUser" 71 | } 72 | } 73 | }, 74 | "ACMCertificate": { 75 | "Type": "AWS::CertificateManager::Certificate", 76 | "Description": "The certificate used to secure the CloudFront distribution", 77 | "Properties": { 78 | "DomainName": "info.uniswap.org", 79 | "ValidationMethod": "DNS", 80 | "Tags": [ 81 | { 82 | "Key": "Name", 83 | "Value": "info.uniswap.org" 84 | }, 85 | { 86 | "Key": "Stack", 87 | "Value": { 88 | "Ref": "AWS::StackName" 89 | } 90 | } 91 | ] 92 | } 93 | }, 94 | "AssetsBucket": { 95 | "Type": "AWS::S3::Bucket", 96 | "Description": "Bucket containing the site deployed files", 97 | "Properties": { 98 | "VersioningConfiguration": { 99 | "Status": "Enabled" 100 | }, 101 | "AccessControl": "Private", 102 | "WebsiteConfiguration": { 103 | "IndexDocument": "index.html" 104 | }, 105 | "Tags": [ 106 | { 107 | "Key": "Template", 108 | "Value": { 109 | "Ref": "AWS::StackName" 110 | } 111 | } 112 | ], 113 | "PublicAccessBlockConfiguration": { 114 | "BlockPublicAcls": true, 115 | "BlockPublicPolicy": true, 116 | "IgnorePublicAcls": true, 117 | "RestrictPublicBuckets": true 118 | } 119 | } 120 | }, 121 | "AssetsBucketPolicy": { 122 | "Type": "AWS::S3::BucketPolicy", 123 | "Description": "Policy that allows the CloudFront access identity to read from the bucket", 124 | "Properties": { 125 | "Bucket": { 126 | "Ref": "AssetsBucket" 127 | }, 128 | "PolicyDocument": { 129 | "Version": "2012-10-17", 130 | "Statement": [ 131 | { 132 | "Sid": "AllowCloudFront", 133 | "Effect": "Allow", 134 | "Principal": { 135 | "AWS": { 136 | "Fn::Join": [ 137 | "", 138 | [ 139 | "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ", 140 | { 141 | "Ref": "CloudFrontAccessIdentity" 142 | } 143 | ] 144 | ] 145 | } 146 | }, 147 | "Action": ["s3:GetObject", "s3:ListBucket"], 148 | "Resource": [ 149 | { 150 | "Fn::Join": [ 151 | "", 152 | [ 153 | { 154 | "Fn::GetAtt": ["AssetsBucket", "Arn"] 155 | }, 156 | "/*" 157 | ] 158 | ] 159 | }, 160 | { 161 | "Fn::GetAtt": ["AssetsBucket", "Arn"] 162 | } 163 | ] 164 | }, 165 | { 166 | "Sid": "DenyPutFromOtherUsers", 167 | "Effect": "Deny", 168 | "NotPrincipal": { 169 | "AWS": { 170 | "Fn::GetAtt": ["DeploymentUser", "Arn"] 171 | } 172 | }, 173 | "Action": "s3:PutObject", 174 | "Resource": { 175 | "Fn::Join": [ 176 | "", 177 | [ 178 | { 179 | "Fn::GetAtt": ["AssetsBucket", "Arn"] 180 | }, 181 | "/*" 182 | ] 183 | ] 184 | } 185 | } 186 | ] 187 | } 188 | } 189 | }, 190 | "CloudFrontAccessIdentity": { 191 | "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", 192 | "Description": "Identity used to read from the assets bucket", 193 | "Properties": { 194 | "CloudFrontOriginAccessIdentityConfig": { 195 | "Comment": { 196 | "Ref": "AWS::StackName" 197 | } 198 | } 199 | } 200 | }, 201 | "CloudFrontDistribution": { 202 | "Type": "AWS::CloudFront::Distribution", 203 | "Description": "Distribution that produces a CNAME for the assets bucket", 204 | "Properties": { 205 | "Tags": [ 206 | { 207 | "Key": "Template", 208 | "Value": { 209 | "Ref": "AWS::StackName" 210 | } 211 | } 212 | ], 213 | "DistributionConfig": { 214 | "CustomErrorResponses": [ 215 | { 216 | "ResponseCode": 200, 217 | "ErrorCode": 404, 218 | "ErrorCachingMinTTL": 0, 219 | "ResponsePagePath": "/" 220 | } 221 | ], 222 | "Comment": "info.uniswap.org", 223 | "Enabled": true, 224 | "Aliases": ["info.uniswap.org"], 225 | "DefaultRootObject": "index.html", 226 | "HttpVersion": "http2", 227 | "Origins": [ 228 | { 229 | "DomainName": { 230 | "Fn::GetAtt": ["AssetsBucket", "DomainName"] 231 | }, 232 | "Id": "assets-bucket", 233 | "S3OriginConfig": { 234 | "OriginAccessIdentity": { 235 | "Fn::Join": [ 236 | "/", 237 | [ 238 | "origin-access-identity", 239 | "cloudfront", 240 | { 241 | "Ref": "CloudFrontAccessIdentity" 242 | } 243 | ] 244 | ] 245 | } 246 | } 247 | } 248 | ], 249 | "ViewerCertificate": { 250 | "AcmCertificateArn": { 251 | "Ref": "ACMCertificate" 252 | }, 253 | "SslSupportMethod": "sni-only" 254 | }, 255 | "DefaultCacheBehavior": { 256 | "Compress": true, 257 | "AllowedMethods": ["GET", "HEAD"], 258 | "ForwardedValues": { 259 | "QueryString": false 260 | }, 261 | "TargetOriginId": "assets-bucket", 262 | "ViewerProtocolPolicy": "redirect-to-https" 263 | } 264 | } 265 | } 266 | } 267 | } 268 | } 269 | --------------------------------------------------------------------------------