├── .yarnrc ├── .env ├── .npmrc ├── public ├── serve.json ├── favicon.png ├── manifest.json └── index.html ├── .vscode └── settings.json ├── src ├── react-app-env.d.ts ├── assets │ ├── eth.png │ ├── MetaMask.png │ ├── debank.png │ ├── zapper.png │ ├── 0xtracker.png │ ├── defiwatch.png │ ├── placeholder.png │ └── logo.svg ├── components │ ├── Emoji │ │ └── index.js │ ├── Column │ │ └── index.js │ ├── Title │ │ └── index.js │ ├── Dashboard │ │ └── index.js │ ├── Row │ │ └── index.js │ ├── LocalLoader │ │ └── index.js │ ├── DoubleLogo │ │ └── index.js │ ├── Toggle │ │ └── index.tsx │ ├── Copy │ │ └── index.js │ ├── Link │ │ └── index.js │ ├── FormattedName │ │ └── index.js │ ├── QuestionHelper │ │ └── index.tsx │ ├── CurrencySelect │ │ └── index.js │ ├── GlobalStats │ │ └── index.js │ ├── Panel │ │ └── index.js │ ├── TokenLogo │ │ └── index.js │ ├── DropdownSelect │ │ └── index.js │ ├── UniPrice │ │ └── index.js │ ├── AdvanceChart │ │ └── index.js │ ├── TokenChart │ │ └── datafeed.js │ ├── Popover │ │ └── index.tsx │ ├── PairChart │ │ └── datafeed.js │ ├── Select │ │ ├── popout.js │ │ ├── index.js │ │ └── styles.js │ ├── index.js │ ├── ButtonStyled │ │ └── index.js │ ├── Warning │ │ └── index.js │ ├── UserChart │ │ └── index.js │ ├── AccountSearch │ │ └── index.js │ ├── PinnedData │ │ └── index.js │ ├── PairReturnsChart │ │ └── index.js │ ├── LPList │ │ └── index.js │ ├── CandleChart │ │ └── index.js │ ├── GlobalChart │ │ └── index.js │ ├── SideNav │ │ └── index.js │ ├── ExportTransactionsButton │ │ └── index.js │ ├── TradingviewChart │ │ └── index.js │ └── Chart │ │ └── index.js ├── apollo │ └── client.js ├── pages │ ├── AllPairsPage.js │ ├── AllTokensPage.js │ ├── AccountLookup.js │ └── GlobalPage.js ├── utils │ ├── data.ts │ └── tokenLists.ts ├── contexts │ ├── V1Data.js │ ├── LocalStorage.js │ └── Application.js ├── index.js ├── hooks │ └── index.ts ├── constants │ ├── index.js │ └── coingecko.js ├── Theme │ └── index.js └── App.js ├── .prettierrc ├── webpack.config.js ├── tsconfig.strict.json ├── .gitignore ├── tsconfig.json ├── .eslintrc.json ├── README.md ├── .github └── workflows │ └── lint.yml └── package.json /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts false 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_GOOGLE_ANALYTICS_ID="" -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NPM_TOKEN} 2 | -------------------------------------------------------------------------------- /public/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "cleanUrls": false 3 | } 4 | -------------------------------------------------------------------------------- /.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.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangolindex/analytics-avalanche/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/assets/eth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangolindex/analytics-avalanche/HEAD/src/assets/eth.png -------------------------------------------------------------------------------- /src/assets/MetaMask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangolindex/analytics-avalanche/HEAD/src/assets/MetaMask.png -------------------------------------------------------------------------------- /src/assets/debank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangolindex/analytics-avalanche/HEAD/src/assets/debank.png -------------------------------------------------------------------------------- /src/assets/zapper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangolindex/analytics-avalanche/HEAD/src/assets/zapper.png -------------------------------------------------------------------------------- /src/assets/0xtracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangolindex/analytics-avalanche/HEAD/src/assets/0xtracker.png -------------------------------------------------------------------------------- /src/assets/defiwatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangolindex/analytics-avalanche/HEAD/src/assets/defiwatch.png -------------------------------------------------------------------------------- /src/assets/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangolindex/analytics-avalanche/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 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Pangolin Info", 3 | "name": "View statistics for the Pangolin exchange.", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 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 | } -------------------------------------------------------------------------------- /.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 | .env 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 | 26 | .idea/ 27 | 28 | public/tradingview-chart -------------------------------------------------------------------------------- /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"] 22 | } 23 | -------------------------------------------------------------------------------- /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 { RowFixed } from '../Row' 7 | import LogoAndTitle from '../../assets/logo_and_title.svg' 8 | 9 | const TitleWrapper = styled.div` 10 | text-decoration: none; 11 | 12 | &:hover { 13 | cursor: pointer; 14 | } 15 | 16 | z-index: 10; 17 | ` 18 | 19 | export default function Title() { 20 | const history = useHistory() 21 | 22 | return ( 23 | history.push('/')}> 24 | 25 | 26 | logo 27 | 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /.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 | # Pangolin Analytics 2 | 3 | An open source analytics platform for Pangolin -- a community-driven decentralized exchange for Avalanche and Ethereum assets with fast settlement, low transaction fees, and a democratic distribution -- powered by Avalanche. 4 | 5 | - Website: [pangolin.exchange](https://pangolin.exchange/) 6 | - Interface: [app.pangolin.exchange](https://app.pangolin.exchange) 7 | - Telegram: [Pangolin](https://t.me/pangolindexV2) 8 | - Discord: [Pangolin](https://discord.com/invite/pangolindex) 9 | - Twitter: [@pangolindex](https://twitter.com/pangolindex) 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 | 23 | ## Attribution 24 | This code was adapted from this Uniswap repo: [uniswap-info](https://github.com/Uniswap/uniswap-info). 25 | -------------------------------------------------------------------------------- /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/LocalLoader/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled, { css, keyframes } from 'styled-components' 3 | 4 | const pulse = keyframes` 5 | 0% { transform: scale(1); } 6 | 60% { transform: scale(1.1); } 7 | 100% { transform: scale(1); } 8 | ` 9 | 10 | const Wrapper = styled.div` 11 | pointer-events: none; 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | height: 100%; 16 | width: 100%; 17 | 18 | ${(props) => 19 | props.fill && !props.height 20 | ? css` 21 | height: 100vh; 22 | ` 23 | : css` 24 | height: 180px; 25 | `} 26 | ` 27 | 28 | const AnimatedImg = styled.div` 29 | animation: ${pulse} 800ms linear infinite; 30 | & > * { 31 | width: 72px; 32 | } 33 | ` 34 | 35 | const LocalLoader = ({ fill }) => { 36 | 37 | return ( 38 | 39 | 40 | loading-icon 41 | 42 | 43 | ) 44 | } 45 | 46 | export default LocalLoader 47 | -------------------------------------------------------------------------------- /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/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://gateway.thegraph.com/api/f2d0632512eab32ee18295ff38a272be/subgraphs/id/BoQbRerZRKwcvjGUXvq7s1MCHPenVtzonv1cVLhn1qe2', 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 stakingClient = new ApolloClient({ 22 | link: new HttpLink({ 23 | uri: 'https://api.thegraph.com/subgraphs/name/way2rach/talisman', 24 | }), 25 | cache: new InMemoryCache(), 26 | shouldBatch: true, 27 | }) 28 | 29 | export const blockClient = new ApolloClient({ 30 | link: new HttpLink({ 31 | uri: 'https://api.studio.thegraph.com/query/59684/avalanche-pangolin-blocks/version/latest', 32 | }), 33 | cache: new InMemoryCache(), 34 | }) 35 | -------------------------------------------------------------------------------- /src/pages/AllPairsPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } 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 } from '../components/Row' 10 | import Search from '../components/Search' 11 | import { useMedia } from 'react-use' 12 | 13 | function AllPairsPage() { 14 | const allPairs = useAllPairData() 15 | 16 | useEffect(() => { 17 | window.scrollTo(0, 0) 18 | }, []) 19 | 20 | const below800 = useMedia('(max-width: 800px)') 21 | 22 | return ( 23 | 24 | 25 | 26 | Top Pairs 27 | {!below800 && } 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | export default AllPairsPage 38 | -------------------------------------------------------------------------------- /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/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 | 13 | function AllTokensPage() { 14 | const allTokens = useAllTokenData() 15 | 16 | useEffect(() => { 17 | window.scrollTo(0, 0) 18 | }, []) 19 | 20 | const below600 = useMedia('(max-width: 800px)') 21 | 22 | return ( 23 | 24 | 25 | 26 | Top Tokens 27 | {!below600 && } 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | export default AllTokensPage 38 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 entity was created in subgraph 16 | // keys are lowercase token addresses <-------- 17 | const TOKEN_OVERRIDES: { [address: string]: { name: string; symbol: string } } = { 18 | '0x4ec58f9d205f9c919920313932cc71ec68d123c7': { 19 | name: 'Splash Token', 20 | symbol: 'SPLASH', 21 | }, 22 | '0xbc6f589171d6d66eb44ebcc92dffb570db4208da': { 23 | name: 'Wave Token', 24 | symbol: 'WAVE', 25 | }, 26 | '0x260bbf5698121eb85e7a74f2e45e16ce762ebe11': { 27 | name: 'Axelar Wrapped UST', 28 | symbol: 'axlUST', 29 | }, 30 | '0xb599c3590f42f8f995ecfa0f85d2980b76862fc1': { 31 | name: 'Wormhole UST', 32 | symbol: 'UST', 33 | }, 34 | } 35 | const TOKEN_SET: Set = new Set(Object.keys(TOKEN_OVERRIDES)) 36 | 37 | // override tokens with incorrect symbol or names 38 | export function updateNameData(data: BasicData): BasicData | undefined { 39 | if (data?.token0?.id && TOKEN_SET.has(data.token0.id)) { 40 | data.token0.name = TOKEN_OVERRIDES[data.token0.id].name 41 | data.token0.symbol = TOKEN_OVERRIDES[data.token0.id].symbol 42 | } 43 | 44 | if (data?.token1?.id && TOKEN_SET.has(data.token1.id)) { 45 | data.token1.name = TOKEN_OVERRIDES[data.token1.id].name 46 | data.token1.symbol = TOKEN_OVERRIDES[data.token1.id].symbol 47 | } 48 | 49 | return data 50 | } 51 | -------------------------------------------------------------------------------- /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 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | Pangolin Analytics 29 | 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/GlobalStats/index.js: -------------------------------------------------------------------------------- 1 | import React 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 { TYPE } from '../../Theme' 9 | import { SWAP_FEE_TO_LP } from '../../constants' 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 { oneDayVolumeUSD, oneDayTxns, pairCount } = useGlobalData() 29 | const [ethPrice] = useEthPrice() 30 | const formattedEthPrice = ethPrice ? formattedNum(ethPrice, true) : '-' 31 | const oneDayFees = oneDayVolumeUSD ? formattedNum(oneDayVolumeUSD * SWAP_FEE_TO_LP, true) : '' 32 | 33 | return ( 34 |
35 | 36 | 37 | {!below400 && ( 38 | 39 | AVAX Price: {formattedEthPrice} 40 | 41 | )} 42 | 43 | {!below1180 && ( 44 | 45 | Transactions (24H): {localNumber(oneDayTxns)} 46 | 47 | )} 48 | {!below1024 && ( 49 | 50 | Pairs: {localNumber(pairCount)} 51 | 52 | )} 53 | {!below1295 && ( 54 | 55 | Fees (24H): {localNumber(oneDayFees)}  56 | 57 | )} 58 | 59 | 60 |
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /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 | if (typeof GOOGLE_ANALYTICS_ID === 'string') { 17 | ReactGA.initialize(GOOGLE_ANALYTICS_ID) 18 | ReactGA.set({ 19 | customBrowserType: !isMobile 20 | ? 'desktop' 21 | : 'web3' in window || 'ethereum' in window 22 | ? 'mobileWeb3' 23 | : 'mobileRegular', 24 | }) 25 | } else { 26 | ReactGA.initialize('test', { testMode: true, debug: true }) 27 | } 28 | 29 | function ContextProviders({ children }) { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | function Updaters() { 46 | return ( 47 | <> 48 | 49 | 50 | 51 | 52 | ) 53 | } 54 | 55 | ReactDOM.render( 56 | 57 | 58 | 59 | <> 60 | 61 | 62 | 63 | 64 | , 65 | document.getElementById('root') 66 | ) 67 | -------------------------------------------------------------------------------- /src/utils/tokenLists.ts: -------------------------------------------------------------------------------- 1 | import { TokenList } from '@pangolindex/token-lists' 2 | import schema from '@pangolindex/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 | if (isLast) throw new Error(`Failed to download list ${listUrl}`) 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/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/TokenLogo/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import styled from 'styled-components' 3 | import PlaceHolder from '../../assets/placeholder.png' 4 | import EthereumLogo from '../../assets/eth.png' 5 | import { getTokenLogo } from '../../utils' 6 | 7 | const BAD_IMAGES = {} 8 | 9 | const Inline = styled.div` 10 | display: flex; 11 | align-items: center; 12 | align-self: center; 13 | ` 14 | 15 | const Image = styled.img` 16 | width: ${({ size }) => size}; 17 | height: ${({ size }) => size}; 18 | /* background-color: white; */ 19 | border-radius: 50%; 20 | box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); 21 | ` 22 | 23 | const StyledEthereumLogo = styled.div` 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | 28 | > img { 29 | width: ${({ size }) => size}; 30 | height: ${({ size }) => size}; 31 | } 32 | ` 33 | 34 | export default function TokenLogo({ address, header = false, size = '24px', ...rest }) { 35 | const [error, setError] = useState(false) 36 | 37 | useEffect(() => { 38 | setError(false) 39 | }, [address]) 40 | 41 | if (error || BAD_IMAGES[address]) { 42 | return ( 43 | 44 | {''} 45 | 46 | ) 47 | } 48 | 49 | // hard coded fixes for trust wallet api issues 50 | if (address?.toLowerCase() === '0x5e74c9036fb86bd7ecdcb084a0673efc32ea31cb') { 51 | address = '0x42456d7084eacf4083f1140d3229471bba2949a8' 52 | } 53 | 54 | if (address?.toLowerCase() === '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f') { 55 | address = '0xc011a72400e58ecd99ee497cf89e3775d4bd732f' 56 | } 57 | 58 | if (address?.toLowerCase() === '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') { 59 | return ( 60 | 61 | 69 | 70 | ) 71 | } 72 | 73 | const path = getTokenLogo(address, 48) 74 | 75 | return ( 76 | 77 | {''} { 83 | BAD_IMAGES[address] = true 84 | setError(true) 85 | event.preventDefault() 86 | }} 87 | /> 88 | 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /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/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/components/AdvanceChart/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react' 2 | import styled from 'styled-components' 3 | import { useDarkModeManager } from '../../contexts/LocalStorage' 4 | import { useMedia } from 'react-use' 5 | import { widget } from '@pangolindex/tradingview-chart' 6 | 7 | const PowerBy = styled.div` 8 | color: ${({ theme }) => theme.text4}; 9 | margin-bottom: 1rem; 10 | position: absolute; 11 | text-align: center; 12 | width: 100%; 13 | bottom: 12px; 14 | 15 | @media screen and (max-width: 600px) { 16 | position: relative; 17 | bottom: 0px; 18 | } 19 | ` 20 | 21 | const AdvanceChart = ({ symbolName, style, datafeed }) => { 22 | const [darkMode] = useDarkModeManager() 23 | const below600 = useMedia('(max-width: 600px)') 24 | const tvWidgetRef = useRef(null) 25 | 26 | useEffect(() => { 27 | const widgetOptions = { 28 | symbol: symbolName || 'PNG', 29 | datafeed: datafeed, 30 | interval: '1D', 31 | container_id: 'tv_chart_container', 32 | library_path: '/tradingview-chart/', 33 | timeframe: '2M', 34 | debug: false, 35 | time_frames: [ 36 | { text: '1Y', resolution: '1W', description: '1 Year', title: '1yr' }, 37 | { text: '1M', resolution: '1D', description: '1 Months' }, 38 | { text: '1D', resolution: 60, description: '1 Day' }, 39 | ], 40 | locale: 'en', 41 | disabled_features: ['use_localstorage_for_settings', 'header_fullscreen_button'], 42 | charts_storage_url: 'https://saveload.tradingview.com', 43 | charts_storage_api_version: '1.1', 44 | fullscreen: false, 45 | autosize: true, 46 | theme: darkMode ? 'Dark' : 'Light', 47 | preset: below600 ? 'mobile' : undefined, 48 | loading_screen: { foregroundColor: '#000000' }, 49 | } 50 | 51 | const tvWidget = new widget(widgetOptions) 52 | tvWidgetRef.current = tvWidget 53 | 54 | return () => { 55 | if (tvWidgetRef.current !== null) { 56 | tvWidgetRef.current.remove() 57 | tvWidgetRef.current = null 58 | } 59 | } 60 | // eslint-disable-next-line react-hooks/exhaustive-deps 61 | }, []) 62 | 63 | useEffect(() => { 64 | const themeName = darkMode ? 'Dark' : 'Light' 65 | 66 | tvWidgetRef.current.onChartReady(() => { 67 | tvWidgetRef.current.changeTheme(themeName, { disableUndo: true }) 68 | }) 69 | // eslint-disable-next-line react-hooks/exhaustive-deps 70 | }, [darkMode]) 71 | 72 | return ( 73 | <> 74 |
75 | 76 | Powered by Tradingview 77 | 78 | ) 79 | } 80 | 81 | export default AdvanceChart 82 | -------------------------------------------------------------------------------- /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 copy from 'copy-to-clipboard' 6 | import { getTokenLogo } from '../utils' 7 | 8 | export function useColor(tokenAddress, token) { 9 | const [color, setColor] = useState('#2172E5') 10 | if (tokenAddress) { 11 | const path = getTokenLogo(tokenAddress, 24) 12 | if (path) { 13 | Vibrant.from(path).getPalette((err, palette) => { 14 | if (palette && palette.Vibrant) { 15 | let detectedHex = palette.Vibrant.hex 16 | let AAscore = hex(detectedHex, '#FFF') 17 | while (AAscore < 3) { 18 | detectedHex = shade(0.005, detectedHex) 19 | AAscore = hex(detectedHex, '#FFF') 20 | } 21 | if (token === 'DAI') { 22 | setColor('#FAAB14') 23 | } else { 24 | setColor(detectedHex) 25 | } 26 | } 27 | }) 28 | } 29 | } 30 | return color 31 | } 32 | 33 | export function useCopyClipboard(timeout = 500) { 34 | const [isCopied, setIsCopied] = useState(false) 35 | 36 | const staticCopy = useCallback((text) => { 37 | const didCopy = copy(text) 38 | setIsCopied(didCopy) 39 | }, []) 40 | 41 | useEffect(() => { 42 | if (isCopied) { 43 | const hide = setTimeout(() => { 44 | setIsCopied(false) 45 | }, timeout) 46 | 47 | return () => { 48 | clearTimeout(hide) 49 | } 50 | } 51 | }, [isCopied, setIsCopied, timeout]) 52 | 53 | return [isCopied, staticCopy] 54 | } 55 | 56 | export const useOutsideClick = (ref, ref2, callback) => { 57 | const handleClick = (e) => { 58 | if (ref.current && ref.current && !ref2.current) { 59 | callback(true) 60 | } else if (ref.current && !ref.current.contains(e.target) && ref2.current && !ref2.current.contains(e.target)) { 61 | callback(true) 62 | } else { 63 | callback(false) 64 | } 65 | } 66 | useEffect(() => { 67 | document.addEventListener('click', handleClick) 68 | return () => { 69 | document.removeEventListener('click', handleClick) 70 | } 71 | }) 72 | } 73 | 74 | export default function useInterval(callback: () => void, delay: null | number) { 75 | const savedCallback = useRef<() => void>() 76 | 77 | // Remember the latest callback. 78 | useEffect(() => { 79 | savedCallback.current = callback 80 | }, [callback]) 81 | 82 | // Set up the interval. 83 | useEffect(() => { 84 | function tick() { 85 | const current = savedCallback.current 86 | current && current() 87 | } 88 | 89 | if (delay !== null) { 90 | tick() 91 | const id = setInterval(tick, delay) 92 | return () => clearInterval(id) 93 | } 94 | return 95 | }, [delay]) 96 | } 97 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const FACTORY_ADDRESS = '0xefa94DE7a4656D787667C749f7E1223D71E9FD88' // new factory 2 | 3 | export const WAVAX_ADDRESS = '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7' 4 | export const PNG_ADDRESS = '0x60781C2586D68229fde47564546784ab3fACA982' 5 | 6 | export const BUNDLE_ID = '1' 7 | 8 | export const SWAP_FEE = 0.003 9 | export const SWAP_FEE_TO_LP = 0.0025 10 | 11 | export const timeframeOptions = { 12 | WEEK: '1 week', 13 | MONTH: '1 month', 14 | // THREE_MONTHS: '3 months', 15 | // YEAR: '1 year', 16 | ALL_TIME: 'All time', 17 | } 18 | 19 | // token list urls to fetch tokens from - use for warnings on tokens and pairs 20 | export const SUPPORTED_LIST_URLS__NO_ENS = [ 21 | 'https://raw.githubusercontent.com/pangolindex/tokenlists/main/pangolin.tokenlist.json', 22 | ] 23 | 24 | // hide from overview list 25 | export const OVERVIEW_TOKEN_BLACKLIST = [ 26 | '0xa47a05ed74f80fa31621612887d26df40bcf0ca9', // Das Coin (DAS) 27 | '0x97b99b4009041e948337ebca7e6ae52f9f6e633c', // Connor Coin (CON) 28 | '0x2bc8834bc217f15df898fcab896e2b3e3673faf6', // SXI - Deprecated for SWAPXI 29 | 30 | // AEB Tokens 31 | '0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15', // ETH 32 | '0xde3a24028580884448a5397872046a019649b084', // USDT 33 | '0x408d4cd0adb7cebd1f1a1c33a0ba2098e1295bab', // WBTC 34 | '0xb3fe5374f67d7a22886a0ee082b2e2f9d2651651', // LINK 35 | '0xba7deebbfc5fa1100fb055a87773e1e99cd3507a', // DAI 36 | '0xf39f9671906d8630812f9d9863bbef5d523c84ab', // UNI 37 | '0x39cf1bd5f15fb22ec3d9ff86b0727afc203427cc', // SUSHI 38 | '0x8ce2dee54bb9921a2ae0a63dbb2df8ed88b91dd9', // AAVE 39 | '0x99519acb025a0e0d44c3875a4bbf03af65933627', // YFI 40 | ] 41 | 42 | export const AEB_TOKEN_ADDRESSES = [ 43 | '0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15', 44 | '0xe54eb2c3009fa411bf24fb017f9725b973ce36f0', 45 | '0x8ce2dee54bb9921a2ae0a63dbb2df8ed88b91dd9', 46 | '0x6b329326e0f6b95b93b52229b213334278d6f277', 47 | '0xaeb044650278731ef3dc244692ab9f64c78ffaea', 48 | '0xb3fe5374f67d7a22886a0ee082b2e2f9d2651651', 49 | '0xba7deebbfc5fa1100fb055a87773e1e99cd3507a', 50 | '0x46c54b16af7747067f412c78ebadae203a26ada0', 51 | '0xe1463e8991c8a62e64b77b5fb6b22f190344c2a9', 52 | '0x39cf1bd5f15fb22ec3d9ff86b0727afc203427cc', 53 | '0x68e44c4619db40ae1a0725e77c02587bc8fbd1c9', 54 | '0xde3a24028580884448a5397872046a019649b084', 55 | '0x390ba0fb0bd3aa2a5484001606329701148074e6', 56 | '0xc84d7bff2555955b44bdf6a307180810412d751b', 57 | '0xf39f9671906d8630812f9d9863bbef5d523c84ab', 58 | '0x408d4cd0adb7cebd1f1a1c33a0ba2098e1295bab', 59 | '0x99519acb025a0e0d44c3875a4bbf03af65933627', 60 | ] 61 | 62 | // pair blacklist 63 | export const PAIR_BLACKLIST = [] 64 | 65 | /** 66 | * For tokens that cause errors on fee calculations 67 | */ 68 | export const FEE_WARNING_TOKENS = [] 69 | 70 | export const PAIR_CHART_VIEW_OPTIONS = { 71 | VOLUME: 'Volume', 72 | LIQUIDITY: 'Liquidity', 73 | RATE0: 'Rate 0', 74 | RATE1: 'Rate 1', 75 | } 76 | -------------------------------------------------------------------------------- /src/components/TokenChart/datafeed.js: -------------------------------------------------------------------------------- 1 | import { getIntervalTokenData } from '../../contexts/TokenData' 2 | import dayjs from 'dayjs' 3 | import { convertIntervalToSeconds } from '../../utils' 4 | 5 | const configurationData = { 6 | supports_search: false, 7 | exchanges: [ 8 | { 9 | value: 'Pangolin', 10 | name: 'Pangolin', 11 | desc: 'Pangolin', 12 | }, 13 | ], 14 | } 15 | 16 | export default (tokenAddress, symbol, base) => { 17 | return { 18 | onReady: (callback) => { 19 | setTimeout(() => callback(configurationData)) 20 | }, 21 | 22 | searchSymbols: async (userInput, exchange, symbolType, onResultReadyCallback) => {}, 23 | 24 | resolveSymbol: async (symbolName, onSymbolResolvedCallback, onResolveErrorCallback) => { 25 | const symbolInfo = { 26 | ticker: symbol, 27 | name: symbol, 28 | description: symbol, 29 | type: 'crypto', 30 | session: '24x7', 31 | timezone: 'Etc/UTC', 32 | exchange: 'Pangolin', 33 | minmov: 1, 34 | pricescale: 100, 35 | has_seconds: true, 36 | has_intraday: true, 37 | has_no_volume: true, 38 | has_daily: true, 39 | has_weekly_and_monthly: true, 40 | has_emtpy_bars: true, 41 | } 42 | 43 | onSymbolResolvedCallback(symbolInfo) 44 | }, 45 | 46 | getBars: async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => { 47 | const { from, to } = periodParams 48 | 49 | try { 50 | const interval = convertIntervalToSeconds(resolution) 51 | let data = await getIntervalTokenData(tokenAddress, from, to, interval, undefined) 52 | 53 | if (data.length === 0) { 54 | // "noData" should be set if there is no data in the requested period. 55 | onHistoryCallback([], { 56 | noData: true, 57 | }) 58 | return 59 | } 60 | let bars = [] 61 | data.forEach((bar) => { 62 | if (bar.timestamp >= from && bar.timestamp < to) { 63 | bars = [ 64 | ...bars, 65 | { 66 | time: bar.timestamp * 1000, 67 | low: parseFloat(bar.open), 68 | high: parseFloat(bar.close), 69 | open: parseFloat(bar.open || 0), 70 | close: parseFloat(bar.close || 0), 71 | }, 72 | ] 73 | } 74 | }) 75 | 76 | if (bars && bars.length > 0) { 77 | bars.push({ 78 | time: dayjs().unix() * 1000, 79 | open: parseFloat(bars[bars.length - 1].close), 80 | close: parseFloat(base), 81 | low: Math.min(parseFloat(base), parseFloat(bars[bars.length - 1].close)), 82 | high: Math.max(parseFloat(base), parseFloat(bars[bars.length - 1].close)), 83 | }) 84 | } 85 | 86 | onHistoryCallback(bars, { 87 | noData: false, 88 | }) 89 | } catch (error) { 90 | console.log('[getBars]: Get error', error) 91 | onErrorCallback(error) 92 | } 93 | }, 94 | 95 | subscribeBars: (symbolInfo, resolution, onRealtimeCallback, subscribeUID, onResetCacheNeededCallback) => {}, 96 | 97 | unsubscribeBars: (subscriberUID) => {}, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /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 | const Arrow = styled.div` 28 | width: 8px; 29 | height: 8px; 30 | z-index: 9998; 31 | 32 | ::before { 33 | position: absolute; 34 | width: 8px; 35 | height: 8px; 36 | z-index: 9998; 37 | 38 | content: ''; 39 | border: 1px solid ${({ theme }) => theme.bg3}; 40 | transform: rotate(45deg); 41 | background: ${({ theme }) => theme.bg2}; 42 | } 43 | 44 | &.arrow-top { 45 | bottom: -5px; 46 | ::before { 47 | border-top: none; 48 | border-left: none; 49 | } 50 | } 51 | 52 | &.arrow-bottom { 53 | top: -5px; 54 | ::before { 55 | border-bottom: none; 56 | border-right: none; 57 | } 58 | } 59 | 60 | &.arrow-left { 61 | right: -5px; 62 | 63 | ::before { 64 | border-bottom: none; 65 | border-left: none; 66 | } 67 | } 68 | 69 | &.arrow-right { 70 | left: -5px; 71 | ::before { 72 | border-right: none; 73 | border-top: none; 74 | } 75 | } 76 | ` 77 | 78 | export interface PopoverProps { 79 | content: React.ReactNode 80 | show: boolean 81 | children: React.ReactNode 82 | placement?: Placement 83 | } 84 | 85 | export default function Popover({ content, show, children, placement = 'auto' }: PopoverProps) { 86 | const [referenceElement, setReferenceElement] = useState(null) 87 | const [popperElement, setPopperElement] = useState(null) 88 | const [arrowElement, setArrowElement] = useState(null) 89 | const { styles, update, attributes } = usePopper(referenceElement, popperElement, { 90 | placement, 91 | strategy: 'fixed', 92 | modifiers: [ 93 | { name: 'offset', options: { offset: [8, 8] } }, 94 | { name: 'arrow', options: { element: arrowElement } }, 95 | ], 96 | }) 97 | 98 | useInterval(update, show ? 100 : null) 99 | 100 | return ( 101 | <> 102 | {children} 103 | 104 | 105 | {content} 106 | 112 | 113 | 114 | 115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /src/components/PairChart/datafeed.js: -------------------------------------------------------------------------------- 1 | import { getHourlyRateData } from '../../contexts/PairData' 2 | import { PAIR_CHART_VIEW_OPTIONS } from '../../constants' 3 | import { convertIntervalToSeconds } from '../../utils' 4 | import dayjs from 'dayjs' 5 | 6 | const configurationData = { 7 | supports_search: false, 8 | exchanges: [ 9 | { 10 | value: 'Pangolin', 11 | name: 'Pangolin', 12 | desc: 'Pangolin', 13 | }, 14 | ], 15 | } 16 | 17 | export default (tokenAddress, symbol, base, pair) => { 18 | return { 19 | onReady: (callback) => { 20 | setTimeout(() => callback(configurationData)) 21 | }, 22 | 23 | searchSymbols: async (userInput, exchange, symbolType, onResultReadyCallback) => {}, 24 | 25 | resolveSymbol: async (symbolName, onSymbolResolvedCallback, onResolveErrorCallback) => { 26 | const symbolInfo = { 27 | ticker: symbol, 28 | name: symbol, 29 | description: symbol, 30 | type: 'crypto', 31 | session: '24x7', 32 | timezone: 'Etc/UTC', 33 | exchange: 'Pangolin', 34 | minmov: 1, 35 | pricescale: 100, 36 | has_seconds: true, 37 | has_intraday: true, 38 | has_no_volume: true, 39 | has_daily: true, 40 | has_weekly_and_monthly: true, 41 | has_emtpy_bars: true, 42 | } 43 | 44 | onSymbolResolvedCallback(symbolInfo) 45 | }, 46 | 47 | getBars: async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => { 48 | const { from, to } = periodParams 49 | 50 | try { 51 | const interval = convertIntervalToSeconds(resolution) 52 | 53 | let pairChartdata = await getHourlyRateData(tokenAddress, from, to, interval, undefined) 54 | 55 | if (pairChartdata.length === 0) { 56 | // "noData" should be set if there is no data in the requested period. 57 | onHistoryCallback([], { 58 | noData: true, 59 | }) 60 | return 61 | } 62 | 63 | let data = 64 | pair === PAIR_CHART_VIEW_OPTIONS.RATE0 && pairChartdata.length > 0 ? pairChartdata?.[0] : pairChartdata?.[1] 65 | let bars = [] 66 | data.forEach((bar) => { 67 | if (bar.timestamp >= from && bar.timestamp < to) { 68 | bars = [ 69 | ...bars, 70 | { 71 | time: bar.timestamp * 1000, 72 | low: parseFloat(bar.open), 73 | high: parseFloat(bar.close), 74 | open: parseFloat(bar.open || 0), 75 | close: parseFloat(bar.close || 0), 76 | }, 77 | ] 78 | } 79 | }) 80 | 81 | if (bars && bars.length > 0) { 82 | bars.push({ 83 | time: dayjs().unix() * 1000, 84 | open: parseFloat(bars[bars.length - 1].close), 85 | close: parseFloat(base), 86 | low: Math.min(parseFloat(base), parseFloat(bars[bars.length - 1].close)), 87 | high: Math.max(parseFloat(base), parseFloat(bars[bars.length - 1].close)), 88 | }) 89 | } 90 | 91 | onHistoryCallback(bars, { 92 | noData: false, 93 | }) 94 | } catch (error) { 95 | console.log('[getBars]: Get error', error) 96 | onErrorCallback(error) 97 | } 98 | }, 99 | 100 | subscribeBars: (symbolInfo, resolution, onRealtimeCallback, subscribeUID, onResetCacheNeededCallback) => {}, 101 | 102 | unsubscribeBars: (subscriberUID) => {}, 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /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/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 IconWrapper = styled.div` 15 | position: absolute; 16 | right: 0; 17 | border-radius: 3px; 18 | height: 16px; 19 | width: 16px; 20 | padding: 0px; 21 | bottom: 0; 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | color: ${({ theme }) => theme.text1}; 26 | 27 | :hover { 28 | cursor: pointer; 29 | opacity: 0.7; 30 | } 31 | ` 32 | 33 | const Hint = ({ children, ...rest }) => ( 34 | 35 | {children} 36 | 37 | ) 38 | 39 | const Address = ({ address, token, ...rest }) => ( 40 | 47 | {address} 48 | 49 | ) 50 | 51 | export const Hover = styled.div` 52 | :hover { 53 | cursor: pointer; 54 | opacity: ${({ fade }) => fade && '0.7'}; 55 | } 56 | ` 57 | 58 | export const StyledIcon = styled.div` 59 | color: ${({ theme }) => theme.text1}; 60 | ` 61 | 62 | const EmptyCard = styled.div` 63 | display: flex; 64 | align-items: center; 65 | justify-content: center; 66 | height: 200px; 67 | border-radius: 20px; 68 | color: ${({ theme }) => theme.text1}; 69 | height: ${({ height }) => height && height}; 70 | ` 71 | 72 | export const SideBar = styled.span` 73 | display: grid; 74 | grid-gap: 24px; 75 | position: sticky; 76 | top: 4rem; 77 | ` 78 | 79 | export const SubNav = styled.ul` 80 | list-style: none; 81 | display: flex; 82 | flex-direction: row; 83 | justify-content: flex-start; 84 | align-items: flex-start; 85 | padding: 0; 86 | margin-bottom: 2rem; 87 | ` 88 | export const SubNavEl = styled.li` 89 | list-style: none; 90 | display: flex; 91 | padding-bottom: 0.5rem; 92 | margin-right: 1rem; 93 | font-weight: ${({ isActive }) => (isActive ? 600 : 500)}; 94 | border-bottom: 1px solid rgba(0, 0, 0, 0); 95 | 96 | :hover { 97 | cursor: pointer; 98 | border-bottom: 1px solid ${({ theme }) => theme.bg3}; 99 | } 100 | ` 101 | 102 | export const PageWrapper = styled.div` 103 | display: flex; 104 | flex-direction: column; 105 | padding-top: 36px; 106 | padding-bottom: 80px; 107 | 108 | @media screen and (max-width: 600px) { 109 | & > * { 110 | padding: 0 12px; 111 | } 112 | } 113 | ` 114 | 115 | export const ContentWrapper = styled.div` 116 | display: grid; 117 | justify-content: start; 118 | align-items: start; 119 | grid-template-columns: 1fr; 120 | grid-gap: 24px; 121 | max-width: 1440px; 122 | width: 100%; 123 | margin: 0 auto; 124 | padding: 0 2rem; 125 | box-sizing: border-box; 126 | @media screen and (max-width: 1180px) { 127 | grid-template-columns: 1fr; 128 | padding: 0 1rem; 129 | } 130 | ` 131 | 132 | export const ContentWrapperLarge = styled.div` 133 | display: grid; 134 | justify-content: start; 135 | align-items: start; 136 | grid-template-columns: 1fr; 137 | grid-gap: 24px; 138 | padding: 0 2rem; 139 | margin: 0 auto; 140 | box-sizing: border-box; 141 | max-width: 1440px; 142 | width: 100%; 143 | 144 | @media screen and (max-width: 1282px) { 145 | grid-template-columns: 1fr; 146 | padding: 0 1rem; 147 | } 148 | ` 149 | 150 | export const FullWrapper = styled.div` 151 | display: grid; 152 | justify-content: start; 153 | align-items: start; 154 | grid-template-columns: 1fr; 155 | grid-gap: 24px; 156 | max-width: 1440px; 157 | width: 100%; 158 | margin: 0 auto; 159 | padding: 0 2rem; 160 | box-sizing: border-box; 161 | 162 | @media screen and (max-width: 1180px) { 163 | grid-template-columns: 1fr; 164 | padding: 0 1rem; 165 | } 166 | ` 167 | 168 | export const FixedMenu = styled.div` 169 | z-index: 99; 170 | width: 100%; 171 | box-sizing: border-box; 172 | padding: 1rem; 173 | box-sizing: border-box; 174 | margin-bottom: 2rem; 175 | max-width: 100vw; 176 | 177 | @media screen and (max-width: 800px) { 178 | margin-bottom: 0; 179 | } 180 | ` 181 | 182 | export { Hint, Divider, Address, EmptyCard } 183 | -------------------------------------------------------------------------------- /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/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 function ArbitraryWarning({ 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 ERC-20 token on Avalanche, 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 Snowtrace, this site automatically tracks analytics for all ERC-20 tokens independent of token 46 | integrity. Please do your own research before interacting with any ERC-20 token. 47 | 48 |
49 | ) : ( 50 | 51 | Anyone can create and name any ERC-20 token on Avalanche, including creating fake versions of existing tokens and 52 | tokens that claim to represent projects that do not have a token. Similar to Snowtrace, this site automatically 53 | tracks analytics for all ERC-20 tokens independent of token integrity. Please do your own research before 54 | interacting with any ERC-20 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 Snowtrace 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 Snowtrace 99 | 100 | 101 | setShow(false)}> 102 | I understand 103 | 104 | 105 | )} 106 | 107 | 108 | ) 109 | } 110 | 111 | export function MigrateWarning({ show }) { 112 | return ( 113 | 114 | 115 | 116 | 117 | 118 | Token Migration Alert 119 | 120 | 121 | 122 | Due to the introduction of the faster, cheaper, and safer AB bridge, assets bridged via the old AEB bridge are 123 | being migrated 1:1 to their new equivalent token. These tokens are still being traded, but should be migrated 124 | for ease of integration with Avalanche dapps. 125 | 126 | 127 |
128 | 129 | 130 | Migrate 131 | 132 | 133 | 134 | 135 | 136 | ) 137 | } 138 | -------------------------------------------------------------------------------- /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: '#FF6B00', 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 | function addAccount(account) { 120 | let newAccounts = state?.[SAVED_ACCOUNTS] 121 | newAccounts.push(account) 122 | updateKey(SAVED_ACCOUNTS, newAccounts) 123 | } 124 | 125 | function removeAccount(account) { 126 | let newAccounts = state?.[SAVED_ACCOUNTS] 127 | let index = newAccounts.indexOf(account) 128 | if (index > -1) { 129 | newAccounts.splice(index, 1) 130 | } 131 | updateKey(SAVED_ACCOUNTS, newAccounts) 132 | } 133 | 134 | return [savedAccounts, addAccount, removeAccount] 135 | } 136 | 137 | export function useSavedPairs() { 138 | const [state, { updateKey }] = useLocalStorageContext() 139 | const savedPairs = state?.[SAVED_PAIRS] 140 | 141 | function addPair(address, token0Address, token1Address, token0Symbol, token1Symbol) { 142 | let newList = state?.[SAVED_PAIRS] 143 | newList[address] = { 144 | address, 145 | token0Address, 146 | token1Address, 147 | token0Symbol, 148 | token1Symbol, 149 | } 150 | updateKey(SAVED_PAIRS, newList) 151 | } 152 | 153 | function removePair(address) { 154 | let newList = state?.[SAVED_PAIRS] 155 | newList[address] = null 156 | updateKey(SAVED_PAIRS, newList) 157 | } 158 | 159 | return [savedPairs, addPair, removePair] 160 | } 161 | 162 | export function useSavedTokens() { 163 | const [state, { updateKey }] = useLocalStorageContext() 164 | const savedTokens = state?.[SAVED_TOKENS] 165 | 166 | function addToken(address, symbol) { 167 | let newList = state?.[SAVED_TOKENS] 168 | newList[address] = { 169 | symbol, 170 | } 171 | updateKey(SAVED_TOKENS, newList) 172 | } 173 | 174 | function removeToken(address) { 175 | let newList = state?.[SAVED_TOKENS] 176 | newList[address] = null 177 | updateKey(SAVED_TOKENS, newList) 178 | } 179 | 180 | return [savedTokens, addToken, removeToken] 181 | } 182 | -------------------------------------------------------------------------------- /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 | 119 | history.push('/account/' + account)}> 120 | {account?.slice(0, 42)} 121 | 122 | removeAccount(account)}> 123 | 124 | 125 | 126 | 127 | 128 | 129 | ) 130 | }) 131 | ) : ( 132 | No saved accounts 133 | )} 134 | 135 | )} 136 | 137 | {small && ( 138 | <> 139 | {'Accounts'} 140 | {savedAccounts?.length > 0 ? ( 141 | savedAccounts.map((account) => { 142 | return ( 143 | 144 | history.push('/account/' + account)}> 145 | {small ? ( 146 | {account?.slice(0, 6) + '...' + account?.slice(38, 42)} 147 | ) : ( 148 | {account?.slice(0, 42)} 149 | )} 150 | 151 | removeAccount(account)}> 152 | 153 | 154 | 155 | 156 | 157 | ) 158 | }) 159 | ) : ( 160 | No pinned wallets 161 | )} 162 | 163 | )} 164 | 165 | 166 | ) 167 | } 168 | 169 | export default withRouter(AccountSearch) 170 | -------------------------------------------------------------------------------- /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/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/pages/GlobalPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } 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 | 27 | const ListOptions = styled(AutoRow)` 28 | height: 40px; 29 | width: 100%; 30 | font-size: 1.25rem; 31 | font-weight: 600; 32 | 33 | @media screen and (max-width: 640px) { 34 | font-size: 1rem; 35 | } 36 | ` 37 | 38 | const GridRow = styled.div` 39 | display: grid; 40 | width: 100%; 41 | grid-template-columns: 1fr 1fr; 42 | column-gap: 6px; 43 | align-items: start; 44 | justify-content: space-between; 45 | ` 46 | 47 | function GlobalPage() { 48 | // get data for lists and totals 49 | const allPairs = useAllPairData() 50 | const allTokens = useAllTokenData() 51 | const transactions = useGlobalTransactions() 52 | const { totalLiquidityUSD, oneDayVolumeUSD, volumeChangeUSD, liquidityChangeUSD } = useGlobalData() 53 | 54 | // breakpoints 55 | const below800 = useMedia('(max-width: 800px)') 56 | 57 | // scrolling refs 58 | 59 | useEffect(() => { 60 | document.querySelector('body').scrollTo({ 61 | behavior: 'smooth', 62 | top: 0, 63 | }) 64 | }, []) 65 | 66 | return ( 67 | 68 | 69 | 70 |
71 | 72 | {below800 ? 'Protocol Analytics' : 'Pangolin Protocol Analytics'} 73 | 74 | 75 | 76 | {below800 && ( // mobile card 77 | 78 | 79 | 80 | 81 | 82 | 83 | Volume (24hrs) 84 |
85 | 86 | 87 | 88 | {formattedNum(oneDayVolumeUSD, true)} 89 | 90 | {formattedPercent(volumeChangeUSD)} 91 | 92 | 93 | 94 | 95 | Total Liquidity 96 |
97 | 98 | 99 | 100 | {formattedNum(totalLiquidityUSD, true)} 101 | 102 | {formattedPercent(liquidityChangeUSD)} 103 | 104 | 105 | 106 | 107 | 108 | 109 | )} 110 | {!below800 && ( 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | )} 120 | {below800 && ( 121 | 122 | 123 | 124 | 125 | 126 | )} 127 | 128 | 129 | Top Tokens 130 | See All 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | Top Pairs 139 | See All 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | Transactions 149 | 150 | 151 | 152 | 153 | 154 |
155 | 156 | 157 | ) 158 | } 159 | 160 | export default withRouter(GlobalPage) 161 | -------------------------------------------------------------------------------- /src/constants/coingecko.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mapping between address and CoinGecko coin id 3 | * Using CoinGecko API: https://api.coingecko.com/api/v3/coins/list 4 | */ 5 | const UNFORMATTED_COIN_ID_MAP = { 6 | // AB Tokenlist 7 | '0x60781C2586D68229fde47564546784ab3fACA982': 'pangolin', // PNG 8 | '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7': 'avalanche', // WAVAX 9 | '0xd501281565bf7789224523144Fe5D98e8B28f267': '1inch', // 1INCH.e 10 | '0x63a72806098Bd3D9520cC43356dD78afe5D386D9': 'aave', // AAVE.e 11 | '0x98443B96EA4b0858FDF3219Cd13e98C7A4690588': 'basic-attention-token', // BAT.e 12 | '0x19860CCB0A68fd4213aB9D8266F7bBf05A8dDe98': 'binance-usd', // BUSD.e 13 | '0xc3048E19E76CB9a3Aa9d77D8C03c29Fc906e2437': 'compound', // COMP.e 14 | '0x249848BeCA43aC405b8102Ec90Dd5F22CA513c06': 'curve-dao-token', // CRV.e 15 | '0xd586E7F844cEa2F87f50152665BCbc2C279D8d70': 'dai', // DAI.e 16 | '0x8a0cAc13c7da965a312f08ea4229c37869e85cB9': 'the-graph', // GRT.e 17 | '0x5947BB275c521040051D82396192181b413227A3': 'chainlink', // LINK.e 18 | '0x88128fd4b259552A9A1D457f435a6527AAb72d42': 'maker', // MKR.e 19 | '0xBeC243C995409E6520D7C41E404da5dEba4b209B': 'synthetix-network-token', // SNX.e 20 | '0x37B608519F91f70F2EeB0e5Ed9AF4061722e4F76': 'sushi', // SUSHI.e 21 | '0xc7B5D72C836e718cDA8888eaf03707fAef675079': 'trustswap', // SWAP.e 22 | '0x3Bd2B1c7ED8D396dbb98DED3aEbb41350a5b2339': 'uma', // UMA.e 23 | '0x8eBAf22B6F053dFFeaf46f4Dd9eFA95D89ba8580': 'uniswap', // UNI.e 24 | '0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664': 'usd-coin', // USDC.e 25 | '0xc7198437980c041c805A1EDcbA50c1Ce5db95118': 'tether', // USDT.e 26 | '0x50b7545627a5162F82A992c33b87aDc75187B218': 'bitcoin', // WBTC.e 27 | '0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB': 'ethereum', // WETH.e 28 | '0x9eAaC1B23d935365bD7b542Fe22cEEe2922f52dc': 'yearn-finance', // YFI.e 29 | '0x596fA47043f99A4e0F122243B841E55375cdE0d2': '0x', // ZRX.e 30 | 31 | // Defi Tokenlist 32 | '0x78ea17559B3D2CF85a7F9C2C704eda119Db5E6dE': 'avaware', // AVE 33 | '0xdb333724fAE72b4253FC3d44c8270CBBC86d147b': undefined, // CABAG 34 | '0x3711c397B6c8F7173391361e27e67d72F252cAad': 'complus-network', // COM 35 | '0x488F73cddDA1DE3664775fFd91623637383D6404': 'yetiswap', // YTS 36 | '0x008E26068B3EB40B443d3Ea88c1fF99B789c10F7': 'zero-exchange', // ZERO 37 | '0xC38f41A296A4493Ff429F1238e030924A1542e50': 'snowball-token', // SNOB 38 | '0x1F1FE1eF06ab30a791d6357FdF0a7361B39b1537': undefined, // SFI 39 | '0x6e7f5C0b9f4432716bDd0a77a3601291b9D9e985': 'spore', // SPORE 40 | '0xe896CDeaAC9615145c0cA09C8Cd5C25bced6384c': 'penguin-finance', // PEFI 41 | '0xC931f61B1534EB21D8c11B24f3f5Ab2471d4aB50': 'any-blocknet', // aaBLOCK 42 | '0x4C9B4E1AC6F24CdE3660D5E4Ef1eBF77C710C084': 'lydia-finance', // LYD 43 | '0x846D50248BAf8b7ceAA9d9B53BFd12d7D7FBB25a': 'verso', // VSO 44 | '0x1ECd47FF4d9598f89721A2866BFEb99505a413Ed': 'avme', // AVME 45 | '0xE9D00cBC5f02614d7281D742E6E815A47ce31107': undefined, // CRACK 46 | '0x65378b697853568dA9ff8EaB60C13E1Ee9f4a654': 'husky-avax', // HUSKY 47 | '0xD606199557c8Ab6F4Cc70bD03FaCc96ca576f142': 'gondola-finance', // GDL 48 | '0x81440C939f2C1E34fc7048E518a637205A632a74': 'cycle-token', // CYCLE 49 | '0xd1c3f94DE7e5B45fa4eDBBA472491a9f4B166FC4': 'avalaunch', // XAVA 50 | '0x8349088C575cA45f5A63947FEAeaEcC41136fA01': undefined, // TESLABTC 51 | '0x4aBBc3275f8419685657C2DD69b8ca2e26F23F8E': undefined, // Diamond 52 | '0x76076880e1EBBcE597e6E15c47386cd34de4930F': 'canopus', // OPUS 53 | '0x8D88e48465F30Acfb8daC0b3E35c9D6D7d36abaf': 'canary', // CNR 54 | '0xa5E59761eBD4436fa4d20E1A27cBa29FB2471Fc6': 'sherpa', // SHERPA 55 | '0x961C8c0B1aaD0c0b10a51FeF6a867E3091BCef17': 'defi-yield-protocol', // DYP 56 | '0xd6070ae98b8069de6B494332d1A1a81B6179D960': 'beefy-finance', // BIFI 57 | '0x264c1383EA520f73dd837F915ef3a732e204a493': 'binance-coin', // BNB 58 | '0xB1466d4cf0DCfC0bCdDcf3500F473cdACb88b56D': 'weble-ecosystem-token', // WET 59 | '0x59414b3089ce2AF0010e7523Dea7E2b35d776ec7': 'yield-yak', // YAK 60 | '0x8729438EB15e2C8B576fCc6AeCdA6A148776C0F5': 'benqi', // QI 61 | '0x9E037dE681CaFA6E661e6108eD9c2bd1AA567Ecd': 'allianceblock', // WALBT 62 | '0x21c5402C3B7d40C89Cc472C9dF5dD7E51BbAb1b1': 'tundra-token', // TUNDRA 63 | '0x595c8481c48894771CE8FaDE54ac6Bf59093F9E8': 'gaj', // GAJ 64 | '0x094bd7B2D99711A1486FB94d4395801C6d0fdDcC': 'teddy-cash', // TEDDY 65 | '0x6e84a6216eA6dACC71eE8E6b0a5B7322EEbC0fDd': 'joe', // JOE 66 | '0xE1C110E1B1b4A1deD0cAf3E42BfBdbB7b5d7cE1C': 'elk-finance', // ELK 67 | '0x9Fda7cEeC4c18008096C2fE2B85F05dc300F94d0': 'marginswap', // MFI 68 | '0xAcD7B3D9c10e97d0efA418903C0c7669E702E4C0': 'eleven-finance', // ELE 69 | '0x440aBbf18c54b2782A4917b80a1746d3A2c2Cce1': 'shibavax', // SHIBX 70 | '0x9eF758aC000a354479e538B8b2f01b917b8e89e7': 'xdollar', // XDO 71 | '0xDd453dBD253fA4E5e745047d93667Ce9DA93bbCF': 'zabu-token', // ZABU 72 | '0xD67de0e0a0Fd7b15dC8348Bb9BE742F3c5850454': 'frax-share', // FXS 73 | '0xF44Fb887334Fa17d2c5c0F970B5D320ab53eD557': 'starter-xyz', // START 74 | '0x62a4f3280C02C8Cc3E9ff984e4aaD94f8F7fEA26': undefined, // BABYPangolin 75 | '0xc12e249FaBe1c5Eb7C558E5F50D187687a244E31': undefined, // BLUE 76 | '0x999c891262ce01f1C1AFD1D46260E4c1E508B243': undefined, // GIVE 77 | '0x6AFD5A1ea4b793CC1526d6Dc7e99A608b356eF7b': 'storm-token', // STORM 78 | '0xf57b80A574297892B64E9a6c997662889b04a73a': undefined, // EXP 79 | '0x8A9B36393633aD77ceb8aebC7768815627B93557': undefined, // SPHERE.e 80 | '0x01C2086faCFD7aA38f69A6Bd8C91BEF3BB5adFCa': 'yay-games', // YAY 81 | '0x397bBd6A0E41bdF4C3F971731E180Db8Ad06eBc1': 'avaxtars-token', // AVXT 82 | '0xae9d2385Ff2E2951Dd4fA061e74c4d3deDD24347': undefined, // TOK 83 | '0xb54f16fB19478766A268F172C9480f8da1a7c9C3': undefined, // TIME 84 | '0x90842eb834cFD2A1DB0b1512B254a18E4D396215': 'good-bridging', // GB 85 | '0x0ebd9537A25f56713E34c45b38F421A1e7191469': 'openocean', // OOE 86 | '0x3709E8615E02C15B096f8a9B460ccb8cA8194e86': 'vee-finance', // VEE 87 | '0x938FE3788222A74924E062120E7BFac829c719Fb': undefined, // APEIN 88 | '0xbe6D6323eA233fD1DBe1fF66c5252170c69fb6c7': undefined, // ZUBAX 89 | '0x69A61f38Df59CBB51962E69C54D39184E21C27Ec': undefined, // PARTY 90 | '0xd039C9079ca7F2a87D632A9C0d7cEa0137bAcFB5': 'ape-x', // APE-X 91 | 92 | '0x61eCd63e42C27415696e10864d70ecEA4aA11289': 'rugpull-prevention', // RUGPULL 93 | } 94 | 95 | // Ensure all address keys are lowercase 96 | export const COIN_ID_MAP = Object.entries(UNFORMATTED_COIN_ID_MAP).reduce( 97 | (map, [address, id]) => ({ ...map, [address.toLowerCase()]: id }), 98 | {} 99 | ) 100 | -------------------------------------------------------------------------------- /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 | 23 | :hover { 24 | cursor: pointer; 25 | opacity: 0.7; 26 | } 27 | ` 28 | 29 | const CandleStickChart = ({ 30 | data, 31 | width, 32 | height = 300, 33 | base, 34 | margin = true, 35 | valueFormatter = (val) => formattedNum(val, true), 36 | }) => { 37 | // reference for DOM element to create with chart 38 | const ref = useRef() 39 | 40 | const formattedData = data?.map((entry) => { 41 | return { 42 | time: parseFloat(entry.timestamp), 43 | open: parseFloat(entry.open), 44 | low: parseFloat(entry.open), 45 | close: parseFloat(entry.close), 46 | high: parseFloat(entry.close), 47 | } 48 | }) 49 | 50 | if (formattedData && formattedData.length > 0) { 51 | formattedData.push({ 52 | time: dayjs().unix(), 53 | open: parseFloat(formattedData[formattedData.length - 1].close), 54 | close: parseFloat(base), 55 | low: Math.min(parseFloat(base), parseFloat(formattedData[formattedData.length - 1].close)), 56 | high: Math.max(parseFloat(base), parseFloat(formattedData[formattedData.length - 1].close)), 57 | }) 58 | } 59 | 60 | // pointer to the chart object 61 | const [chartCreated, setChartCreated] = useState(false) 62 | const dataPrev = usePrevious(data) 63 | 64 | const [darkMode] = useDarkModeManager() 65 | const textColor = darkMode ? 'white' : 'black' 66 | const previousTheme = usePrevious(darkMode) 67 | 68 | // reset the chart if theme switches 69 | useEffect(() => { 70 | // eslint-disable-next-line react/prop-types 71 | if (chartCreated && (previousTheme !== darkMode || data.length !== dataPrev.length)) { 72 | // remove the tooltip element 73 | let tooltip = document.getElementById('tooltip-id') 74 | let node = document.getElementById('test-id') 75 | node.removeChild(tooltip) 76 | chartCreated.resize(0, 0) 77 | setChartCreated() 78 | } 79 | }, [chartCreated, darkMode, previousTheme, data, dataPrev]) 80 | 81 | // if no chart created yet, create one with options and add to DOM manually 82 | useEffect(() => { 83 | if (!chartCreated) { 84 | const chart = createChart(ref.current, { 85 | width: width, 86 | height: height, 87 | layout: { 88 | backgroundColor: 'transparent', 89 | textColor: textColor, 90 | }, 91 | grid: { 92 | vertLines: { 93 | color: 'rgba(197, 203, 206, 0.5)', 94 | }, 95 | horzLines: { 96 | color: 'rgba(197, 203, 206, 0.5)', 97 | }, 98 | }, 99 | crosshair: { 100 | mode: CrosshairMode.Normal, 101 | }, 102 | rightPriceScale: { 103 | borderColor: 'rgba(197, 203, 206, 0.8)', 104 | visible: true, 105 | }, 106 | timeScale: { 107 | borderColor: 'rgba(197, 203, 206, 0.8)', 108 | }, 109 | localization: { 110 | priceFormatter: (val) => formattedNum(val), 111 | }, 112 | }) 113 | 114 | var candleSeries = chart.addCandlestickSeries({ 115 | upColor: 'green', 116 | downColor: 'red', 117 | borderDownColor: 'red', 118 | borderUpColor: 'green', 119 | wickDownColor: 'red', 120 | wickUpColor: 'green', 121 | }) 122 | 123 | candleSeries.setData(formattedData) 124 | 125 | var toolTip = document.createElement('div') 126 | toolTip.setAttribute('id', 'tooltip-id') 127 | toolTip.className = 'three-line-legend' 128 | ref.current.appendChild(toolTip) 129 | toolTip.style.display = 'block' 130 | toolTip.style.left = (margin ? 116 : 10) + 'px' 131 | toolTip.style.top = 50 + 'px' 132 | toolTip.style.backgroundColor = 'transparent' 133 | 134 | // get the title of the chart 135 | function setLastBarText() { 136 | toolTip.innerHTML = base 137 | ? `
` + valueFormatter(base) + '
' 138 | : '' 139 | } 140 | setLastBarText() 141 | 142 | // update the title when hovering on the chart 143 | chart.subscribeCrosshairMove(function (param) { 144 | if ( 145 | param === undefined || 146 | param.time === undefined || 147 | param.point.x < 0 || 148 | param.point.x > width || 149 | param.point.y < 0 || 150 | param.point.y > height 151 | ) { 152 | setLastBarText() 153 | } else { 154 | var price = param.seriesPrices.get(candleSeries).close 155 | const time = dayjs.utc(dayjs.unix(param.time)).format('MM/DD h:mm A') 156 | toolTip.innerHTML = 157 | `
` + 158 | valueFormatter(price) + 159 | `` + 160 | time + 161 | ' UTC' + 162 | '' + 163 | '
' 164 | } 165 | }) 166 | 167 | chart.timeScale().fitContent() 168 | 169 | setChartCreated(chart) 170 | } 171 | }, [chartCreated, formattedData, width, height, valueFormatter, base, margin, textColor]) 172 | 173 | // responsiveness 174 | useEffect(() => { 175 | if (width) { 176 | chartCreated && chartCreated.resize(width, height) 177 | chartCreated && chartCreated.timeScale().scrollToPosition(0) 178 | } 179 | }, [chartCreated, height, width]) 180 | 181 | return ( 182 |
183 |
184 | 185 | { 187 | chartCreated && chartCreated.timeScale().fitContent() 188 | }} 189 | /> 190 | 191 |
192 | ) 193 | } 194 | 195 | export default CandleStickChart 196 | -------------------------------------------------------------------------------- /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 LIQUIDITY_BASE = { 23 | USD: 'USD', 24 | AVAX: 'AVAX', 25 | } 26 | const GlobalChart = ({ display }) => { 27 | // chart options 28 | const [chartView, setChartView] = useState(display === 'volume' ? CHART_VIEW.VOLUME : CHART_VIEW.LIQUIDITY) 29 | 30 | // time window and window size for chart 31 | const timeWindow = timeframeOptions.ALL_TIME 32 | const [volumeWindow, setVolumeWindow] = useState(VOLUME_WINDOW.DAYS) 33 | const [liquidityBase, setLiquidityBase] = useState(LIQUIDITY_BASE.USD) 34 | 35 | // global historical data 36 | const [dailyData, weeklyData] = useGlobalChartData() 37 | const { 38 | totalLiquidityUSD, 39 | totalLiquidityETH, 40 | oneDayVolumeUSD, 41 | volumeChangeUSD, 42 | liquidityChangeUSD, 43 | liquidityChangeETH, 44 | oneWeekVolume, 45 | weeklyVolumeChange, 46 | } = useGlobalData() 47 | 48 | // based on window, get starttime 49 | let utcStartTime = getTimeframe(timeWindow) 50 | 51 | const chartDataFiltered = useMemo(() => { 52 | let currentData = volumeWindow === VOLUME_WINDOW.DAYS ? dailyData : weeklyData 53 | return ( 54 | currentData && 55 | Object.keys(currentData) 56 | ?.map((key) => { 57 | let item = currentData[key] 58 | if (item.date > utcStartTime) { 59 | return item 60 | } else { 61 | return undefined 62 | } 63 | }) 64 | .filter((item) => { 65 | return !!item 66 | }) 67 | ) 68 | }, [dailyData, utcStartTime, volumeWindow, weeklyData]) 69 | const below800 = useMedia('(max-width: 800px)') 70 | 71 | // update the width on a window resize 72 | const ref = useRef() 73 | const isClient = typeof window === 'object' 74 | const [width, setWidth] = useState(ref?.current?.container?.clientWidth) 75 | useEffect(() => { 76 | if (!isClient) { 77 | return false 78 | } 79 | function handleResize() { 80 | setWidth(ref?.current?.container?.clientWidth ?? width) 81 | } 82 | window.addEventListener('resize', handleResize) 83 | return () => window.removeEventListener('resize', handleResize) 84 | }, [isClient, width]) // Empty array ensures that effect is only run on mount and unmount 85 | 86 | return chartDataFiltered ? ( 87 | <> 88 | {below800 && ( 89 | 90 | )} 91 | 92 | {chartDataFiltered && chartView === CHART_VIEW.LIQUIDITY && ( 93 | 94 | 104 | 105 | )} 106 | {chartDataFiltered && chartView === CHART_VIEW.VOLUME && ( 107 | 108 | 118 | 119 | )} 120 | {chartView === CHART_VIEW.VOLUME && ( 121 | 129 | setVolumeWindow(VOLUME_WINDOW.DAYS)} 132 | > 133 | D 134 | 135 | setVolumeWindow(VOLUME_WINDOW.WEEKLY)} 139 | > 140 | W 141 | 142 | 143 | )} 144 | {chartView === CHART_VIEW.LIQUIDITY && ( 145 | 153 | setLiquidityBase(LIQUIDITY_BASE.USD)} 156 | > 157 | USD 158 | 159 | setLiquidityBase(LIQUIDITY_BASE.AVAX)} 163 | > 164 | AVAX 165 | 166 | 167 | )} 168 | 169 | ) : ( 170 | '' 171 | ) 172 | } 173 | 174 | export default GlobalChart 175 | -------------------------------------------------------------------------------- /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 ? '#FF6B00' : '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' : '#FF6B00', 52 | primary2: darkMode ? '#3680E7' : '#FF6B00', 53 | primary3: darkMode ? '#4D8FEA' : '#FF6B00', 54 | primary4: darkMode ? '#376bad70' : '#FF6B00', 55 | primary5: darkMode ? '#153d6f70' : '#FF6B00', 56 | 57 | // color text 58 | primaryText1: darkMode ? '#6da8ff' : '#FF6B00', 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%, #FF6B00 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: 9999; 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/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://app.pangolin.exchange" target="_blank"> 168 | Pangolin 169 | </Link> 170 | </HeaderText> 171 | <HeaderText> 172 | <Link href="https://discord.com/pangolindex" target="_blank"> 173 | Discord 174 | </Link> 175 | </HeaderText> 176 | <HeaderText> 177 | <Link href="https://twitter.com/pangolindex" target="_blank"> 178 | Twitter 179 | </Link> 180 | </HeaderText> 181 | <Toggle isActive={isDark} toggle={toggleDarkMode} /> 182 | </AutoColumn> 183 | {!below1180 && ( 184 | <Polling style={{ marginLeft: '.5rem' }}> 185 | <PollingDot /> 186 | <a href="/" style={{ color: 'white' }}> 187 | <TYPE.small color={'white'}> 188 | Updated {!!seconds ? seconds + 's' : '-'} ago <br /> 189 | </TYPE.small> 190 | </a> 191 | </Polling> 192 | )} 193 | </DesktopWrapper> 194 | ) : ( 195 | <MobileWrapper> 196 | <Title /> 197 | </MobileWrapper> 198 | )} 199 | </Wrapper> 200 | ) 201 | } 202 | 203 | export default withRouter(SideNav) 204 | -------------------------------------------------------------------------------- /src/components/ExportTransactionsButton/index.js: -------------------------------------------------------------------------------- 1 | import { writeToString } from '@fast-csv/format' 2 | import PropTypes from 'prop-types' 3 | import React, { useState } from 'react' 4 | import { Download, Loader } from 'react-feather' 5 | 6 | import { updateNameData } from '../../utils/data' 7 | 8 | import { ButtonDark } from '../ButtonStyled' 9 | import { StyledIcon } from '../index.js' 10 | 11 | function prepareTransactionsForExport(transactions) { 12 | const mints = transactions.mints.map((mint) => ({ 13 | date: parseInt(mint.transaction.timestamp, 10) * 1000, 14 | hash: mint.transaction.id, 15 | fiat_amount: mint.amountUSD, 16 | fiat_currency: 'USD', 17 | token1_amount: mint.amount0, 18 | token1_currency: updateNameData(mint.pair).token0.symbol, 19 | token2_amount: mint.amount1, 20 | token2_currency: updateNameData(mint.pair).token1.symbol, 21 | type: 'add', 22 | })) 23 | 24 | const burns = transactions.burns.map((burn) => ({ 25 | date: parseInt(burn.transaction.timestamp, 10) * 1000, 26 | hash: burn.transaction.id, 27 | fiat_amount: burn.amountUSD, 28 | fiat_currency: 'USD', 29 | token1_amount: burn.amount0, 30 | token1_currency: updateNameData(burn.pair).token0.symbol, 31 | token2_amount: burn.amount1, 32 | token2_currency: updateNameData(burn.pair).token1.symbol, 33 | type: 'remove', 34 | })) 35 | 36 | const swaps = transactions.swaps.map((swap) => { 37 | const newSwap = { ...swap } 38 | 39 | // TODO: We should really be using a number library because JS is bad at maths. 40 | const netToken0 = swap.amount0In - swap.amount0Out 41 | const netToken1 = swap.amount1In - swap.amount1Out 42 | if (netToken0 < 0) { 43 | newSwap.token0Symbol = updateNameData(swap.pair).token0.symbol 44 | newSwap.token1Symbol = updateNameData(swap.pair).token1.symbol 45 | newSwap.token0Amount = Math.abs(netToken0) 46 | newSwap.token1Amount = Math.abs(netToken1) 47 | } else if (netToken1 < 0) { 48 | newSwap.token0Symbol = updateNameData(swap.pair).token1.symbol 49 | newSwap.token1Symbol = updateNameData(swap.pair).token0.symbol 50 | newSwap.token0Amount = Math.abs(netToken1) 51 | newSwap.token1Amount = Math.abs(netToken0) 52 | } 53 | 54 | return { 55 | date: parseInt(newSwap.transaction.timestamp, 10) * 1000, 56 | hash: newSwap.transaction.id, 57 | fiat_amount: newSwap.amountUSD, 58 | fiat_currency: 'USD', 59 | token1_amount: newSwap.token0Amount, 60 | token1_currency: newSwap.token0Symbol, 61 | token2_amount: newSwap.token1Amount, 62 | token2_currency: newSwap.token1Symbol, 63 | type: 'swap', 64 | } 65 | }) 66 | 67 | return [...mints, ...burns, ...swaps] 68 | .sort((a, b) => a.date - b.date) 69 | .map(({ date, ...rest }) => ({ date: new Date(date).toISOString(), ...rest })) 70 | } 71 | 72 | function createTransactionExport(transactions) { 73 | const rows = [Object.keys(transactions[0]), ...transactions.map(Object.values)] 74 | return writeToString(rows) 75 | } 76 | 77 | function downloadTransactionExport(fileString) { 78 | const file = new File([fileString], 'transactions.csv', { type: 'text/plain' }) 79 | const fileUrl = window.URL.createObjectURL(file) 80 | const a = document.createElement('a') 81 | a.href = fileUrl 82 | a.download = 'transactions.csv' 83 | document.body.appendChild(a) 84 | a.click() 85 | setTimeout(() => { 86 | window.URL.revokeObjectURL(fileUrl) 87 | a.remove() 88 | }, 0) 89 | } 90 | 91 | const DownloadButton = ({ onClick }) => ( 92 | <ButtonDark 93 | color={'rgba(255, 255, 255, 0.2)'} 94 | style={{ 95 | // TODO: Replace developer styling with actual designs 96 | padding: '4px 11px', 97 | borderRadius: '100px', 98 | alignItems: 'center', 99 | justifyContent: 'center', 100 | }} 101 | onClick={onClick} 102 | > 103 | <StyledIcon> 104 | <Download style={{ opacity: 0.4 }} size="18" /> 105 | </StyledIcon> 106 | </ButtonDark> 107 | ) 108 | 109 | DownloadButton.propTypes = { 110 | onClick: PropTypes.func, 111 | } 112 | 113 | const LoadingButton = () => ( 114 | <ButtonDark 115 | color={'rgba(255, 255, 255, 0.2)'} 116 | style={{ 117 | // TODO: Replace developer styling with actual designs 118 | padding: '4px 11px', 119 | borderRadius: '100px', 120 | alignItems: 'center', 121 | justifyContent: 'center', 122 | }} 123 | > 124 | <StyledIcon> 125 | <Loader style={{ opacity: 0.4 }} size="18" /> 126 | </StyledIcon> 127 | </ButtonDark> 128 | ) 129 | 130 | const PreparedDownloadButton = ({ url }) => ( 131 | <a download="transactions.csv" href={url}> 132 | <ButtonDark 133 | color={'rgba(255, 255, 255, 0.2)'} 134 | style={{ 135 | // TODO: Replace developer styling with actual designs 136 | padding: '4px 11px', 137 | borderRadius: '100px', 138 | alignItems: 'center', 139 | justifyContent: 'center', 140 | }} 141 | > 142 | <StyledIcon> 143 | <Download style={{ opacity: 0.4 }} size="18" color="#27AE60" /> 144 | </StyledIcon> 145 | </ButtonDark> 146 | </a> 147 | ) 148 | 149 | PreparedDownloadButton.propTypes = { 150 | url: PropTypes.string, 151 | } 152 | 153 | const ExportTransactionsButton = ({ transactions }) => { 154 | const [transactionsPreparing, setTransactionsPreparing] = useState(false) 155 | const { mints, burns, swaps } = transactions 156 | 157 | const prepareTransactions = () => { 158 | setTransactionsPreparing(true) 159 | const preparedTransactions = prepareTransactionsForExport(transactions) 160 | createTransactionExport(preparedTransactions) 161 | .then((fileString) => { 162 | downloadTransactionExport(fileString) 163 | setTransactionsPreparing(false) 164 | }) 165 | .catch((err) => { 166 | console.error('Failed to create transaction export', err.stack || err) 167 | setTransactionsPreparing(false) 168 | }) 169 | } 170 | 171 | if (!mints?.length && !burns?.length && !swaps?.length) { 172 | return null 173 | } 174 | 175 | if (transactionsPreparing) { 176 | return <LoadingButton /> 177 | } 178 | 179 | return <DownloadButton onClick={prepareTransactions} /> 180 | } 181 | 182 | const transactionShape = PropTypes.shape({ 183 | __typename: PropTypes.oneOf(['Burn', 'Mint', 'Swap']), 184 | amount0: PropTypes.string, 185 | amount1: PropTypes.string, 186 | amountUSD: PropTypes.string, 187 | id: PropTypes.string, 188 | liquidity: PropTypes.string, 189 | pair: PropTypes.shape({ 190 | __typename: PropTypes.oneOf(['Pair']), 191 | id: PropTypes.string, 192 | token0: PropTypes.shape({ 193 | __typename: PropTypes.oneOf(['Token']), 194 | symbol: PropTypes.string, 195 | }), 196 | token1: PropTypes.shape({ 197 | __typename: PropTypes.oneOf(['Token']), 198 | symbol: PropTypes.string, 199 | }), 200 | }), 201 | sender: PropTypes.string, 202 | to: PropTypes.string, 203 | transaction: PropTypes.shape({ 204 | __typename: PropTypes.oneOf(['Transaction']), 205 | id: PropTypes.string, 206 | timestamp: PropTypes.string, 207 | }), 208 | }) 209 | 210 | ExportTransactionsButton.propTypes = { 211 | transactions: PropTypes.shape({ 212 | burns: PropTypes.arrayOf(transactionShape), 213 | mints: PropTypes.arrayOf(transactionShape), 214 | swaps: PropTypes.arrayOf(transactionShape), 215 | }).isRequired, 216 | } 217 | 218 | export { ExportTransactionsButton } 219 | -------------------------------------------------------------------------------- /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, Redirect, HashRouter } 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 { PAIR_BLACKLIST } from './constants' 19 | import LocalLoader from './components/LocalLoader' 20 | import { useLatestBlocks } from './contexts/Application' 21 | 22 | const AppWrapper = styled.div` 23 | position: relative; 24 | width: 100%; 25 | ` 26 | const ContentWrapper = styled.div` 27 | display: grid; 28 | grid-template-columns: ${({ open }) => (open ? '220px 1fr 200px' : '220px 1fr 64px')}; 29 | 30 | @media screen and (max-width: 1400px) { 31 | grid-template-columns: 220px 1fr; 32 | } 33 | 34 | @media screen and (max-width: 1080px) { 35 | grid-template-columns: 1fr; 36 | max-width: 100vw; 37 | overflow: hidden; 38 | grid-gap: 0; 39 | } 40 | ` 41 | 42 | const Right = styled.div` 43 | position: fixed; 44 | right: 0; 45 | bottom: 0rem; 46 | z-index: 99; 47 | width: ${({ open }) => (open ? '220px' : '64px')}; 48 | height: ${({ open }) => (open ? 'fit-content' : '64px')}; 49 | overflow: auto; 50 | background-color: ${({ theme }) => theme.bg1}; 51 | @media screen and (max-width: 1400px) { 52 | display: none; 53 | } 54 | ` 55 | 56 | const Center = styled.div` 57 | height: 100%; 58 | z-index: 9999; 59 | transition: width 0.25s ease; 60 | background-color: ${({ theme }) => theme.onlyLight}; 61 | ` 62 | 63 | const WarningWrapper = styled.div` 64 | width: 100%; 65 | display: flex; 66 | justify-content: center; 67 | ` 68 | 69 | const WarningBanner = styled.div` 70 | background-color: #ff6871; 71 | padding: 1.5rem; 72 | color: white; 73 | width: 100%; 74 | text-align: center; 75 | font-weight: 500; 76 | ` 77 | 78 | /** 79 | * Wrap the component with the header and sidebar pinned tab 80 | */ 81 | const LayoutWrapper = ({ children, savedOpen, setSavedOpen }) => { 82 | return ( 83 | <> 84 | <ContentWrapper open={savedOpen}> 85 | <SideNav /> 86 | <Center id="center">{children}</Center> 87 | <Right open={savedOpen}> 88 | <PinnedData open={savedOpen} setSavedOpen={setSavedOpen} /> 89 | </Right> 90 | </ContentWrapper> 91 | </> 92 | ) 93 | } 94 | 95 | const BLOCK_DIFFERENCE_THRESHOLD = 30 96 | 97 | function App() { 98 | const [savedOpen, setSavedOpen] = useState(false) 99 | 100 | const globalData = useGlobalData() 101 | const globalChartData = useGlobalChartData() 102 | const [latestBlock, headBlock] = useLatestBlocks() 103 | 104 | // show warning 105 | const showWarning = headBlock && latestBlock ? headBlock - latestBlock > BLOCK_DIFFERENCE_THRESHOLD : false 106 | 107 | return ( 108 | <ApolloProvider client={client}> 109 | <AppWrapper> 110 | {showWarning && ( 111 | <WarningWrapper> 112 | <WarningBanner> 113 | {`Warning: The data on this site has only synced to Avalanche block ${latestBlock} (out of ${headBlock}). Please check back soon.`} 114 | </WarningBanner> 115 | </WarningWrapper> 116 | )} 117 | {latestBlock && 118 | globalData && 119 | Object.keys(globalData).length > 0 && 120 | globalChartData && 121 | Object.keys(globalChartData).length > 0 ? ( 122 | <HashRouter> 123 | <Switch> 124 | <Route 125 | exacts 126 | strict 127 | path="/token/:tokenAddress" 128 | render={({ match }) => { 129 | // if (OVERVIEW_TOKEN_BLACKLIST.includes(match.params.tokenAddress.toLowerCase())) { 130 | // return <Redirect to="/home" /> 131 | // } 132 | if (isAddress(match.params.tokenAddress.toLowerCase())) { 133 | return ( 134 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 135 | <TokenPage address={match.params.tokenAddress.toLowerCase()} /> 136 | </LayoutWrapper> 137 | ) 138 | } else { 139 | return <Redirect to="/home" /> 140 | } 141 | }} 142 | /> 143 | <Route 144 | exacts 145 | strict 146 | path="/pair/:pairAddress" 147 | render={({ match }) => { 148 | if (PAIR_BLACKLIST.includes(match.params.pairAddress.toLowerCase())) { 149 | return <Redirect to="/home" /> 150 | } 151 | if (isAddress(match.params.pairAddress.toLowerCase())) { 152 | return ( 153 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 154 | <PairPage pairAddress={match.params.pairAddress.toLowerCase()} /> 155 | </LayoutWrapper> 156 | ) 157 | } else { 158 | return <Redirect to="/home" /> 159 | } 160 | }} 161 | /> 162 | <Route 163 | exacts 164 | strict 165 | path="/account/:accountAddress" 166 | render={({ match }) => { 167 | if (isAddress(match.params.accountAddress.toLowerCase())) { 168 | return ( 169 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 170 | <AccountPage account={match.params.accountAddress.toLowerCase()} /> 171 | </LayoutWrapper> 172 | ) 173 | } else { 174 | return <Redirect to="/home" /> 175 | } 176 | }} 177 | /> 178 | 179 | <Route path="/home"> 180 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 181 | <GlobalPage /> 182 | </LayoutWrapper> 183 | </Route> 184 | 185 | <Route path="/tokens"> 186 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 187 | <AllTokensPage /> 188 | </LayoutWrapper> 189 | </Route> 190 | 191 | <Route path="/pairs"> 192 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 193 | <AllPairsPage /> 194 | </LayoutWrapper> 195 | </Route> 196 | 197 | <Route path="/accounts"> 198 | <LayoutWrapper savedOpen={savedOpen} setSavedOpen={setSavedOpen}> 199 | <AccountLookup /> 200 | </LayoutWrapper> 201 | </Route> 202 | 203 | <Redirect to="/home" /> 204 | </Switch> 205 | </HashRouter> 206 | ) : ( 207 | <LocalLoader fill="true" /> 208 | )} 209 | </AppWrapper> 210 | </ApolloProvider> 211 | ) 212 | } 213 | 214 | export default App 215 | -------------------------------------------------------------------------------- /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 | isUSD = true, 30 | base, 31 | baseChange, 32 | field, 33 | title, 34 | width, 35 | useWeekly = false, 36 | }) => { 37 | // reference for DOM element to create with chart 38 | const ref = useRef() 39 | 40 | // pointer to the chart object 41 | const [chartCreated, setChartCreated] = useState(false) 42 | 43 | // parse the data and format for tradingview consumption 44 | const formattedData = data?.map((entry) => { 45 | return { 46 | time: dayjs.unix(entry.date).utc().format('YYYY-MM-DD'), 47 | value: parseFloat(entry[field]), 48 | } 49 | }) 50 | 51 | // adjust the scale based on the type of chart 52 | const topScale = type === CHART_TYPES.AREA ? 0.32 : 0.2 53 | 54 | const [darkMode] = useDarkModeManager() 55 | const textColor = darkMode ? 'white' : 'black' 56 | const previousTheme = usePrevious(darkMode) 57 | const previousIsUSD = usePrevious(isUSD) 58 | const previousUseWeekly = usePrevious(useWeekly) 59 | 60 | // reset the chart when required 61 | useEffect(() => { 62 | if (chartCreated && (isUSD !== previousIsUSD || useWeekly !== previousUseWeekly || darkMode !== previousTheme)) { 63 | // remove the tooltip element 64 | let tooltip = document.getElementById('tooltip-id' + type) 65 | let node = document.getElementById('test-id' + type) 66 | node.removeChild(tooltip) 67 | chartCreated.resize(0, 0) 68 | setChartCreated() 69 | } 70 | }, [chartCreated, isUSD, previousIsUSD, useWeekly, previousUseWeekly, darkMode, previousTheme, type]) 71 | 72 | // if no chart created yet, create one with options and add to DOM manually 73 | useEffect(() => { 74 | if (!chartCreated && formattedData) { 75 | var chart = createChart(ref.current, { 76 | width: width, 77 | height: HEIGHT, 78 | layout: { 79 | backgroundColor: 'transparent', 80 | textColor: textColor, 81 | }, 82 | rightPriceScale: { 83 | scaleMargins: { 84 | top: topScale, 85 | bottom: 0, 86 | }, 87 | borderVisible: false, 88 | }, 89 | timeScale: { 90 | borderVisible: false, 91 | }, 92 | grid: { 93 | horzLines: { 94 | color: 'rgba(197, 203, 206, 0.5)', 95 | visible: false, 96 | }, 97 | vertLines: { 98 | color: 'rgba(197, 203, 206, 0.5)', 99 | visible: false, 100 | }, 101 | }, 102 | crosshair: { 103 | horzLine: { 104 | visible: false, 105 | labelVisible: false, 106 | }, 107 | vertLine: { 108 | visible: true, 109 | style: 0, 110 | width: 2, 111 | color: 'rgba(32, 38, 46, 0.1)', 112 | labelVisible: false, 113 | }, 114 | }, 115 | localization: { 116 | priceFormatter: (val) => formattedNum(val, isUSD), 117 | }, 118 | }) 119 | 120 | var series = 121 | type === CHART_TYPES.BAR 122 | ? chart.addHistogramSeries({ 123 | color: '#E1AA00', 124 | priceFormat: { 125 | type: 'volume' 126 | }, 127 | scaleMargins: { 128 | top: 0.32, 129 | bottom: 0 130 | }, 131 | lineColor: '#E1AA00', 132 | lineWidth: 3 133 | }) 134 | : chart.addAreaSeries({ 135 | topColor: '#FFC800', 136 | bottomColor: 'rgba(232, 65, 66, 0)', 137 | lineColor: '#E1AA00', 138 | lineWidth: 3 139 | }) 140 | 141 | series.setData(formattedData) 142 | var toolTip = document.createElement('div') 143 | toolTip.setAttribute('id', 'tooltip-id' + type) 144 | toolTip.className = darkMode ? 'three-line-legend-dark' : 'three-line-legend' 145 | ref.current.appendChild(toolTip) 146 | toolTip.style.display = 'block' 147 | toolTip.style.fontWeight = '500' 148 | toolTip.style.left = -4 + 'px' 149 | toolTip.style.top = '-' + 8 + 'px' 150 | toolTip.style.backgroundColor = 'transparent' 151 | 152 | // format numbers 153 | let percentChange = baseChange?.toFixed(2) 154 | let formattedPercentChange = percentChange ? ((percentChange > 0 ? '+' : '') + percentChange + '%') : '' 155 | let color = percentChange >= 0 ? 'green' : 'red' 156 | 157 | // get the title of the chart 158 | function setLastBarText() { 159 | toolTip.innerHTML = 160 | `<div style="font-size: 16px; margin: 4px 0px; color: ${textColor};">${title} ${type === CHART_TYPES.BAR && !useWeekly ? '(24hr)' : '' 161 | }</div>` + 162 | `<div style="font-size: 22px; margin: 4px 0px; color:${textColor}" >` + 163 | formattedNum(base ?? 0, isUSD) + 164 | `<span style="margin-left: 10px; font-size: 16px; color: ${color};">${formattedPercentChange}</span>` + 165 | '</div>' 166 | } 167 | setLastBarText() 168 | 169 | // update the title when hovering on the chart 170 | chart.subscribeCrosshairMove(function (param) { 171 | if ( 172 | param === undefined || 173 | param.time === undefined || 174 | param.point.x < 0 || 175 | param.point.x > width || 176 | param.point.y < 0 || 177 | param.point.y > HEIGHT 178 | ) { 179 | setLastBarText() 180 | } else { 181 | let dateStr = useWeekly 182 | ? dayjs(param.time.year + '-' + param.time.month + '-' + param.time.day) 183 | .startOf('week') 184 | .format('MMMM D, YYYY') + 185 | '-' + 186 | dayjs(param.time.year + '-' + param.time.month + '-' + param.time.day) 187 | .endOf('week') 188 | .format('MMMM D, YYYY') 189 | : dayjs(param.time.year + '-' + param.time.month + '-' + param.time.day).format('MMMM D, YYYY') 190 | var price = param.seriesPrices.get(series) 191 | 192 | toolTip.innerHTML = 193 | `<div style="font-size: 16px; margin: 4px 0px; color: ${textColor};">${title}</div>` + 194 | `<div style="font-size: 22px; margin: 4px 0px; color: ${textColor}">` + 195 | formattedNum(price, isUSD) + 196 | '</div>' + 197 | '<div>' + 198 | dateStr + 199 | '</div>' 200 | } 201 | }) 202 | 203 | chart.timeScale().fitContent() 204 | 205 | setChartCreated(chart) 206 | } 207 | }, [ 208 | base, 209 | baseChange, 210 | isUSD, 211 | chartCreated, 212 | darkMode, 213 | data, 214 | formattedData, 215 | textColor, 216 | title, 217 | topScale, 218 | type, 219 | useWeekly, 220 | width, 221 | ]) 222 | 223 | // responsiveness 224 | useEffect(() => { 225 | if (width) { 226 | chartCreated && chartCreated.resize(width, HEIGHT) 227 | chartCreated && chartCreated.timeScale().scrollToPosition(0) 228 | } 229 | }, [chartCreated, width]) 230 | 231 | return ( 232 | <Wrapper> 233 | <div ref={ref} id={'test-id' + type} /> 234 | <IconWrapper> 235 | <Play 236 | onClick={() => { 237 | chartCreated && chartCreated.timeScale().fitContent() 238 | }} 239 | /> 240 | </IconWrapper> 241 | </Wrapper> 242 | ) 243 | } 244 | 245 | export default TradingViewChart 246 | -------------------------------------------------------------------------------- /src/components/Chart/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { Area, XAxis, YAxis, ResponsiveContainer, Bar, BarChart, CartesianGrid, Tooltip, AreaChart } from 'recharts' 3 | import styled from 'styled-components' 4 | import { useMedia } from 'react-use' 5 | import { toK, toNiceDate, toNiceDateYear } from '../../utils' 6 | 7 | const ChartWrapper = styled.div` 8 | padding-top: 1em; 9 | margin-left: -1.5em; 10 | @media (max-width: 40em) { 11 | margin-left: -1em; 12 | } 13 | ` 14 | 15 | const Chart = ({ data, chartOption, currencyUnit, symbol }) => { 16 | const [chartData, setChartData] = useState([]) 17 | useEffect(() => { 18 | setChartData([]) 19 | setChartData(data) 20 | }, [data, chartOption, currencyUnit]) 21 | 22 | const isMobile = useMedia('(max-width: 40em)') 23 | if (chartOption === 'price' && chartData && data) { 24 | return ( 25 | <ChartWrapper> 26 | <ResponsiveContainer aspect={isMobile ? 60 / 22 : 60 / 12}> 27 | <AreaChart margin={{ top: 0, right: 0, bottom: 6, left: 10 }} barCategoryGap={1} data={chartData}> 28 | <CartesianGrid stroke="#f5f5f5" /> 29 | <XAxis 30 | tickLine={false} 31 | axisLine={false} 32 | interval="preserveEnd" 33 | tickMargin={14} 34 | minTickGap={80} 35 | tickFormatter={(tick) => toNiceDate(tick)} 36 | dataKey="dayString" 37 | /> 38 | <YAxis 39 | hide={isMobile} 40 | type="number" 41 | tickMargin={16} 42 | orientation="left" 43 | tickFormatter={(tick) => toK(tick)} 44 | axisLine={false} 45 | tickLine={false} 46 | interval="preserveEnd" 47 | minTickGap={80} 48 | yAxisId={2} 49 | /> 50 | <YAxis 51 | hide={true} 52 | type="number" 53 | tickMargin={16} 54 | orientation="left" 55 | tickFormatter={(tick) => toK(tick)} 56 | axisLine={false} 57 | tickLine={false} 58 | interval="preserveEnd" 59 | minTickGap={80} 60 | yAxisId={3} 61 | /> 62 | <Area 63 | strokeWidth={2} 64 | dot={false} 65 | type="monotone" 66 | name={currencyUnit === 'ETH' ? 'Price (ETH/' + symbol + ')' : 'Price (USD/' + symbol + ')'} 67 | dataKey={currencyUnit === 'ETH' ? 'ethPerToken' : 'tokenPriceUSD'} 68 | yAxisId={2} 69 | fill="var(--c-token)" 70 | opacity={'0.4'} 71 | stroke="var(--c-token)" 72 | /> 73 | <Area 74 | strokeWidth={2} 75 | dot={false} 76 | type="monotone" 77 | name={currencyUnit === 'USD' ? 'Inverse (' + symbol + '/USD)' : 'Inverse (' + symbol + '/ETH)'} 78 | dataKey={currencyUnit === 'USD' ? 'tokensPerUSD' : 'tokensPerEth'} 79 | yAxisId={3} 80 | fill="var(--c-token)" 81 | opacity={'0'} 82 | stroke="var(--c-token)" 83 | /> 84 | <Tooltip 85 | cursor={true} 86 | formatter={(val) => toK(val, true)} 87 | labelFormatter={(label) => toNiceDateYear(label)} 88 | labelStyle={{ paddingTop: 4 }} 89 | contentStyle={{ 90 | padding: '10px 14px', 91 | borderRadius: 10, 92 | borderColor: 'var(--c-zircon)', 93 | }} 94 | wrapperStyle={{ top: -70, left: -10 }} 95 | /> 96 | </AreaChart> 97 | </ResponsiveContainer> 98 | </ChartWrapper> 99 | ) 100 | } 101 | if (chartOption !== 'volume' && chartData && data) { 102 | return ( 103 | <ChartWrapper> 104 | <ResponsiveContainer aspect={isMobile ? 60 / 22 : 60 / 12}> 105 | <AreaChart margin={{ top: 0, right: 0, bottom: 6, left: 10 }} barCategoryGap={1} data={chartData}> 106 | <CartesianGrid stroke="#f5f5f5" /> 107 | <XAxis 108 | tickLine={false} 109 | axisLine={false} 110 | interval="preserveEnd" 111 | tickMargin={14} 112 | minTickGap={80} 113 | tickFormatter={(tick) => toNiceDate(tick)} 114 | dataKey="dayString" 115 | /> 116 | <YAxis 117 | hide={isMobile} 118 | type="number" 119 | tickMargin={16} 120 | orientation="left" 121 | tickFormatter={(tick) => toK(tick)} 122 | axisLine={false} 123 | tickLine={false} 124 | interval="preserveEnd" 125 | minTickGap={80} 126 | yAxisId={0} 127 | /> 128 | <YAxis 129 | hide={true} 130 | type="number" 131 | tickMargin={16} 132 | orientation="right" 133 | tickFormatter={(tick) => toK(tick)} 134 | axisLine={false} 135 | tickLine={false} 136 | interval="preserveEnd" 137 | minTickGap={80} 138 | yAxisId={1} 139 | /> 140 | <Tooltip 141 | cursor={true} 142 | formatter={(val) => toK(val, true)} 143 | labelFormatter={(label) => toNiceDateYear(label)} 144 | labelStyle={{ paddingTop: 4 }} 145 | contentStyle={{ 146 | padding: '10px 14px', 147 | borderRadius: 10, 148 | borderColor: 'var(--c-zircon)', 149 | }} 150 | wrapperStyle={{ top: -70, left: -10 }} 151 | /> 152 | <Area 153 | strokeWidth={2} 154 | dot={false} 155 | type="monotone" 156 | name={'Total Liquidity' + (currencyUnit === 'USD' ? ' (USD)' : ' (ETH)')} 157 | dataKey={currencyUnit === 'USD' ? 'usdLiquidity' : 'ethLiquidity'} 158 | yAxisId={0} 159 | fill="var(--c-token)" 160 | opacity={'0.4'} 161 | stroke="var(--c-token)" 162 | /> 163 | <Area 164 | type="monotone" 165 | name={'Eth Balance'} 166 | dataKey={'ethBalance'} 167 | fill="var(--c-token)" 168 | opacity={'0'} 169 | stroke="var(--c-token)" 170 | /> 171 | <Area 172 | type="monotone" 173 | name={'Token Balance'} 174 | dataKey={'tokenBalance'} 175 | fill="var(--c-token)" 176 | yAxisId={1} 177 | opacity={'0'} 178 | stroke="var(--c-token)" 179 | /> 180 | </AreaChart> 181 | </ResponsiveContainer> 182 | </ChartWrapper> 183 | ) 184 | } else { 185 | // volume 186 | return ( 187 | <ChartWrapper> 188 | <ResponsiveContainer aspect={isMobile ? 60 / 22 : 60 / 12}> 189 | <BarChart margin={{ top: 0, right: 0, bottom: 6, left: 10 }} barCategoryGap={1} data={chartData}> 190 | <CartesianGrid stroke="#f5f5f5" /> 191 | <XAxis 192 | tickLine={false} 193 | axisLine={false} 194 | interval="preserveEnd" 195 | minTickGap={80} 196 | tickMargin={14} 197 | tickFormatter={(tick) => toNiceDate(tick)} 198 | dataKey="dayString" 199 | /> 200 | <YAxis 201 | hide={isMobile} 202 | type="number" 203 | axisLine={false} 204 | tickMargin={16} 205 | tickFormatter={(tick) => toK(tick)} 206 | tickLine={false} 207 | interval="preserveEnd" 208 | minTickGap={80} 209 | yAxisId={0} 210 | /> 211 | <Tooltip 212 | cursor={true} 213 | formatter={(val) => toK(val, true)} 214 | labelFormatter={(label) => toNiceDateYear(label)} 215 | labelStyle={{ paddingTop: 4 }} 216 | contentStyle={{ 217 | padding: '10px 14px', 218 | borderRadius: 10, 219 | borderColor: 'var(--c-zircon)', 220 | }} 221 | wrapperStyle={{ top: -70, left: -10 }} 222 | /> 223 | <Bar 224 | type="monotone" 225 | name={'Volume' + (currencyUnit === 'USD' ? ' (USD)' : ' (ETH)')} 226 | dataKey={currencyUnit === 'USD' ? 'usdVolume' : 'ethVolume'} 227 | fill="var(--c-token)" 228 | opacity={'0.4'} 229 | yAxisId={0} 230 | stroke="var(--c-token)" 231 | /> 232 | </BarChart> 233 | </ResponsiveContainer> 234 | </ChartWrapper> 235 | ) 236 | } 237 | } 238 | 239 | export default Chart 240 | -------------------------------------------------------------------------------- /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 { client, healthClient } from '../apollo/client' 7 | import { SUBGRAPH_HEALTH, SUBGRAPH_LATEST_BLOCK } from '../apollo/queries' 8 | import { ethers } from 'ethers' 9 | dayjs.extend(utc) 10 | 11 | const UPDATE = 'UPDATE' 12 | const UPDATE_TIMEFRAME = 'UPDATE_TIMEFRAME' 13 | const UPDATE_SESSION_START = 'UPDATE_SESSION_START' 14 | const UPDATED_SUPPORTED_TOKENS = 'UPDATED_SUPPORTED_TOKENS' 15 | const UPDATE_LATEST_BLOCK = 'UPDATE_LATEST_BLOCK' 16 | const UPDATE_HEAD_BLOCK = 'UPDATE_HEAD_BLOCK' 17 | 18 | const SUPPORTED_TOKENS = 'SUPPORTED_TOKENS' 19 | const TIME_KEY = 'TIME_KEY' 20 | const CURRENCY = 'CURRENCY' 21 | const SESSION_START = 'SESSION_START' 22 | const LATEST_BLOCK = 'LATEST_BLOCK' 23 | const HEAD_BLOCK = 'HEAD_BLOCK' 24 | 25 | const ApplicationContext = createContext() 26 | 27 | function useApplicationContext() { 28 | return useContext(ApplicationContext) 29 | } 30 | 31 | function reducer(state, { type, payload }) { 32 | switch (type) { 33 | case UPDATE: { 34 | const { currency } = payload 35 | return { 36 | ...state, 37 | [CURRENCY]: currency, 38 | } 39 | } 40 | case UPDATE_TIMEFRAME: { 41 | const { newTimeFrame } = payload 42 | return { 43 | ...state, 44 | [TIME_KEY]: newTimeFrame, 45 | } 46 | } 47 | case UPDATE_SESSION_START: { 48 | const { timestamp } = payload 49 | return { 50 | ...state, 51 | [SESSION_START]: timestamp, 52 | } 53 | } 54 | 55 | case UPDATE_LATEST_BLOCK: { 56 | const { block } = payload 57 | return { 58 | ...state, 59 | [LATEST_BLOCK]: block, 60 | } 61 | } 62 | 63 | case UPDATE_HEAD_BLOCK: { 64 | const { block } = payload 65 | return { 66 | ...state, 67 | [HEAD_BLOCK]: block, 68 | } 69 | } 70 | 71 | case UPDATED_SUPPORTED_TOKENS: { 72 | const { supportedTokens } = payload 73 | return { 74 | ...state, 75 | [SUPPORTED_TOKENS]: supportedTokens, 76 | } 77 | } 78 | 79 | default: { 80 | throw Error(`Unexpected action type in DataContext reducer: '${type}'.`) 81 | } 82 | } 83 | } 84 | 85 | const INITIAL_STATE = { 86 | CURRENCY: 'USD', 87 | TIME_KEY: timeframeOptions.ALL_TIME, 88 | } 89 | 90 | export default function Provider({ children }) { 91 | const [state, dispatch] = useReducer(reducer, INITIAL_STATE) 92 | const update = useCallback((currency) => { 93 | dispatch({ 94 | type: UPDATE, 95 | payload: { 96 | currency, 97 | }, 98 | }) 99 | }, []) 100 | 101 | // global time window for charts - see timeframe options in constants 102 | const updateTimeframe = useCallback((newTimeFrame) => { 103 | dispatch({ 104 | type: UPDATE_TIMEFRAME, 105 | payload: { 106 | newTimeFrame, 107 | }, 108 | }) 109 | }, []) 110 | 111 | // used for refresh button 112 | const updateSessionStart = useCallback((timestamp) => { 113 | dispatch({ 114 | type: UPDATE_SESSION_START, 115 | payload: { 116 | timestamp, 117 | }, 118 | }) 119 | }, []) 120 | 121 | const updateSupportedTokens = useCallback((supportedTokens) => { 122 | dispatch({ 123 | type: UPDATED_SUPPORTED_TOKENS, 124 | payload: { 125 | supportedTokens, 126 | }, 127 | }) 128 | }, []) 129 | 130 | const updateLatestBlock = useCallback((block) => { 131 | dispatch({ 132 | type: UPDATE_LATEST_BLOCK, 133 | payload: { 134 | block, 135 | }, 136 | }) 137 | }, []) 138 | 139 | const updateHeadBlock = useCallback((block) => { 140 | dispatch({ 141 | type: UPDATE_HEAD_BLOCK, 142 | payload: { 143 | block, 144 | }, 145 | }) 146 | }, []) 147 | 148 | return ( 149 | <ApplicationContext.Provider 150 | value={useMemo( 151 | () => [ 152 | state, 153 | { 154 | update, 155 | updateSessionStart, 156 | updateTimeframe, 157 | updateSupportedTokens, 158 | updateLatestBlock, 159 | updateHeadBlock, 160 | }, 161 | ], 162 | [ 163 | state, 164 | update, 165 | updateTimeframe, 166 | updateSessionStart, 167 | updateSupportedTokens, 168 | updateLatestBlock, 169 | updateHeadBlock, 170 | ] 171 | )} 172 | > 173 | {children} 174 | </ApplicationContext.Provider> 175 | ) 176 | } 177 | 178 | export function useLatestBlocks() { 179 | const [state, { updateLatestBlock, updateHeadBlock }] = useApplicationContext() 180 | 181 | const latestBlock = state?.[LATEST_BLOCK] 182 | const headBlock = state?.[HEAD_BLOCK] 183 | 184 | useEffect(() => { 185 | async function fetch() { 186 | try { 187 | const res = await healthClient.query({ 188 | query: SUBGRAPH_HEALTH, 189 | }) 190 | const syncedBlock = res.data.indexingStatusForCurrentVersion.chains[0].latestBlock.number 191 | const headBlock = res.data.indexingStatusForCurrentVersion.chains[0].chainHeadBlock.number 192 | if (syncedBlock && headBlock) { 193 | updateLatestBlock(syncedBlock) 194 | updateHeadBlock(headBlock) 195 | } 196 | } catch (e) { 197 | console.log(e) 198 | } 199 | } 200 | async function altFetch() { 201 | try { 202 | const [ 203 | { 204 | data: { 205 | _meta: { 206 | block: { number: syncedBlock }, 207 | }, 208 | }, 209 | }, 210 | headBlock, 211 | ] = await Promise.all([ 212 | client.query({ 213 | query: SUBGRAPH_LATEST_BLOCK, 214 | }), 215 | new ethers.providers.JsonRpcProvider('https://api.avax.network/ext/bc/C/rpc').getBlockNumber(), 216 | ]) 217 | if (syncedBlock && headBlock) { 218 | updateLatestBlock(syncedBlock) 219 | updateHeadBlock(headBlock) 220 | } 221 | } catch (e) { 222 | console.error(e) 223 | } 224 | } 225 | if (!latestBlock) { 226 | altFetch() 227 | // fetch() 228 | } 229 | }, [latestBlock, updateHeadBlock, updateLatestBlock]) 230 | 231 | return [latestBlock, headBlock] 232 | } 233 | 234 | export function useCurrentCurrency() { 235 | const [state, { update }] = useApplicationContext() 236 | const toggleCurrency = useCallback(() => { 237 | if (state.currency === 'ETH') { 238 | update('USD') 239 | } else { 240 | update('ETH') 241 | } 242 | }, [state, update]) 243 | return [state[CURRENCY], toggleCurrency] 244 | } 245 | 246 | export function useTimeframe() { 247 | const [state, { updateTimeframe }] = useApplicationContext() 248 | const activeTimeframe = state?.[TIME_KEY] 249 | return [activeTimeframe, updateTimeframe] 250 | } 251 | 252 | export function useStartTimestamp() { 253 | const [activeWindow] = useTimeframe() 254 | const [startDateTimestamp, setStartDateTimestamp] = useState() 255 | 256 | // monitor the old date fetched 257 | useEffect(() => { 258 | let startTime = 259 | dayjs 260 | .utc() 261 | .subtract( 262 | 1, 263 | activeWindow === timeframeOptions.week ? 'week' : activeWindow === timeframeOptions.ALL_TIME ? 'year' : 'year' 264 | ) 265 | .startOf('day') 266 | .unix() - 1 267 | // if we find a new start time less than the current startrtime - update oldest pooint to fetch 268 | setStartDateTimestamp(startTime) 269 | }, [activeWindow, startDateTimestamp]) 270 | 271 | return startDateTimestamp 272 | } 273 | 274 | // keep track of session length for refresh ticker 275 | export function useSessionStart() { 276 | const [state, { updateSessionStart }] = useApplicationContext() 277 | const sessionStart = state?.[SESSION_START] 278 | 279 | useEffect(() => { 280 | if (!sessionStart) { 281 | updateSessionStart(Date.now()) 282 | } 283 | }) 284 | 285 | const [seconds, setSeconds] = useState(0) 286 | 287 | useEffect(() => { 288 | let interval = null 289 | interval = setInterval(() => { 290 | setSeconds(Date.now() - sessionStart ?? Date.now()) 291 | }, 1000) 292 | 293 | return () => clearInterval(interval) 294 | }, [seconds, sessionStart]) 295 | 296 | return parseInt(seconds / 1000) 297 | } 298 | 299 | export function useListedTokens() { 300 | const [state, { updateSupportedTokens }] = useApplicationContext() 301 | const supportedTokens = state?.[SUPPORTED_TOKENS] 302 | 303 | useEffect(() => { 304 | async function fetchList() { 305 | const allFetched = await SUPPORTED_LIST_URLS__NO_ENS.reduce(async (fetchedTokens, url) => { 306 | const tokensSoFar = await fetchedTokens 307 | const newTokens = await getTokenList(url) 308 | return Promise.resolve([...tokensSoFar, ...newTokens.tokens]) 309 | }, Promise.resolve([])) 310 | let formatted = allFetched?.map((t) => t.address.toLowerCase()) 311 | updateSupportedTokens(formatted) 312 | } 313 | if (!supportedTokens) { 314 | fetchList() 315 | } 316 | }, [updateSupportedTokens, supportedTokens]) 317 | 318 | return supportedTokens 319 | } 320 | --------------------------------------------------------------------------------