(
76 |
77 | {option.logo}
78 | {option.label}
79 |
80 | )}
81 | styles={isMobile ? customStylesMobile : customStyles}
82 | {...rest}
83 | components={{
84 | DropdownIndicator: () => (
85 |
86 | 🔎
87 |
88 | ),
89 | Menu: ({ children, innerRef, innerProps }) => {
90 | return (
91 |
92 |
93 | {
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 |
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 |
119 |
120 |
124 |
125 |
126 |
136 |
137 |
138 |
148 |
149 |
150 |
151 |
161 |
162 |
163 | )}
164 |
165 |
166 |
167 |
168 | Pangolin
169 |
170 |
171 |
172 |
173 | Discord
174 |
175 |
176 |
177 |
178 | Twitter
179 |
180 |
181 |
182 |
183 | {!below1180 && (
184 |
185 |
186 |
187 |
188 | Updated {!!seconds ? seconds + 's' : '-'} ago
189 |
190 |
191 |
192 | )}
193 |
194 | ) : (
195 |
196 |
197 |
198 | )}
199 |
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 |
103 |
104 |
105 |
106 |
107 | )
108 |
109 | DownloadButton.propTypes = {
110 | onClick: PropTypes.func,
111 | }
112 |
113 | const LoadingButton = () => (
114 |
124 |
125 |
126 |
127 |
128 | )
129 |
130 | const PreparedDownloadButton = ({ url }) => (
131 |
132 |
142 |
143 |
144 |
145 |
146 |
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
177 | }
178 |
179 | return
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 |
85 |
86 | {children}
87 |
88 |
89 |
90 |
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 |
109 |
110 | {showWarning && (
111 |
112 |
113 | {`Warning: The data on this site has only synced to Avalanche block ${latestBlock} (out of ${headBlock}). Please check back soon.`}
114 |
115 |
116 | )}
117 | {latestBlock &&
118 | globalData &&
119 | Object.keys(globalData).length > 0 &&
120 | globalChartData &&
121 | Object.keys(globalChartData).length > 0 ? (
122 |
123 |
124 | {
129 | // if (OVERVIEW_TOKEN_BLACKLIST.includes(match.params.tokenAddress.toLowerCase())) {
130 | // return
131 | // }
132 | if (isAddress(match.params.tokenAddress.toLowerCase())) {
133 | return (
134 |
135 |
136 |
137 | )
138 | } else {
139 | return
140 | }
141 | }}
142 | />
143 | {
148 | if (PAIR_BLACKLIST.includes(match.params.pairAddress.toLowerCase())) {
149 | return
150 | }
151 | if (isAddress(match.params.pairAddress.toLowerCase())) {
152 | return (
153 |
154 |
155 |
156 | )
157 | } else {
158 | return
159 | }
160 | }}
161 | />
162 | {
167 | if (isAddress(match.params.accountAddress.toLowerCase())) {
168 | return (
169 |
170 |
171 |
172 | )
173 | } else {
174 | return
175 | }
176 | }}
177 | />
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 | ) : (
207 |
208 | )}
209 |
210 |
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 | `${title} ${type === CHART_TYPES.BAR && !useWeekly ? '(24hr)' : ''
161 | }
` +
162 | `` +
163 | formattedNum(base ?? 0, isUSD) +
164 | `${formattedPercentChange}` +
165 | '
'
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 | `${title}
` +
194 | `` +
195 | formattedNum(price, isUSD) +
196 | '
' +
197 | '' +
198 | dateStr +
199 | '
'
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 |
233 |
234 |
235 | {
237 | chartCreated && chartCreated.timeScale().fitContent()
238 | }}
239 | />
240 |
241 |
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 |
26 |
27 |
28 |
29 | toNiceDate(tick)}
36 | dataKey="dayString"
37 | />
38 | toK(tick)}
44 | axisLine={false}
45 | tickLine={false}
46 | interval="preserveEnd"
47 | minTickGap={80}
48 | yAxisId={2}
49 | />
50 | toK(tick)}
56 | axisLine={false}
57 | tickLine={false}
58 | interval="preserveEnd"
59 | minTickGap={80}
60 | yAxisId={3}
61 | />
62 |
73 |
84 | 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 |
97 |
98 |
99 | )
100 | }
101 | if (chartOption !== 'volume' && chartData && data) {
102 | return (
103 |
104 |
105 |
106 |
107 | toNiceDate(tick)}
114 | dataKey="dayString"
115 | />
116 | toK(tick)}
122 | axisLine={false}
123 | tickLine={false}
124 | interval="preserveEnd"
125 | minTickGap={80}
126 | yAxisId={0}
127 | />
128 | toK(tick)}
134 | axisLine={false}
135 | tickLine={false}
136 | interval="preserveEnd"
137 | minTickGap={80}
138 | yAxisId={1}
139 | />
140 | 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 |
163 |
171 |
180 |
181 |
182 |
183 | )
184 | } else {
185 | // volume
186 | return (
187 |
188 |
189 |
190 |
191 | toNiceDate(tick)}
198 | dataKey="dayString"
199 | />
200 | toK(tick)}
206 | tickLine={false}
207 | interval="preserveEnd"
208 | minTickGap={80}
209 | yAxisId={0}
210 | />
211 | 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 |
232 |
233 |
234 |
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 | [
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 |
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 |
--------------------------------------------------------------------------------