1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

Merge pull request #1020 from oceanprotocol/v4-pool-graph

Pool chart updates, combine all pool data subgraph queries
This commit is contained in:
Matthias Kretschmann 2022-01-31 13:58:57 +00:00 committed by GitHub
commit 2fb7ed3516
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 2192 additions and 4890 deletions

6432
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,15 +27,15 @@
"@tippyjs/react": "^4.2.6",
"@urql/introspection": "^0.3.1",
"@walletconnect/web3-provider": "^1.7.1",
"axios": "^0.24.0",
"axios": "^0.25.0",
"bignumber.js": "^9.0.2",
"chart.js": "^2.9.4",
"chart.js": "^3.7.0",
"classnames": "^2.3.1",
"d3": "^7.3.0",
"date-fns": "^2.28.0",
"decimal.js": "^10.3.1",
"dom-confetti": "^0.2.2",
"dotenv": "^14.1.0",
"dotenv": "^15.0.0",
"ethereum-address": "0.0.4",
"ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^8.0.6",
@ -46,11 +46,11 @@
"jwt-decode": "^3.1.2",
"lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0",
"next": "^12.0.8",
"next": "^12.0.9",
"query-string": "^7.1.0",
"querystring": "^0.2.1",
"react": "^17.0.2",
"react-chartjs-2": "^2.11.2",
"react-chartjs-2": "^4.0.1",
"react-clipboard.js": "^2.0.16",
"react-data-table-component": "^6.11.7",
"react-dom": "^17.0.2",
@ -65,22 +65,22 @@
"remark-html": "^13.0.1",
"remove-markdown": "^0.3.0",
"slugify": "^1.6.5",
"swr": "^1.1.2",
"urql": "^2.0.6",
"swr": "^1.2.0",
"urql": "^2.1.1",
"use-dark-mode": "^2.3.1",
"web3": "^1.6.1",
"web3modal": "^1.9.5",
"yup": "^0.32.11"
},
"devDependencies": {
"@svgr/webpack": "^6.2.0",
"@svgr/webpack": "^6.2.1",
"@types/chart.js": "^2.9.35",
"@types/d3": "^7.1.0",
"@types/js-cookie": "^3.0.1",
"@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3",
"@types/lodash.omit": "^4.5.6",
"@types/node": "^17.0.8",
"@types/node": "^17.0.13",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",
"@types/react-modal": "^3.13.1",

View File

@ -25,7 +25,7 @@ import {
} from '../@types/subgraph/PoolShares'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
import { UserSalesQuery as UsersSalesList } from '../@types/subgraph/UserSalesQuery'
import { PoolLiquidity } from 'src/@types/subgraph/PoolLiquidity'
import { PoolData } from 'src/@types/subgraph/PoolData'
export interface UserLiquidity {
price: string
@ -285,9 +285,14 @@ const TopSalesQuery = gql`
}
`
const poolLiquidityQuery = gql`
query PoolLiquidity($pool: ID!, $owner: String!) {
pool(id: $pool) {
const poolDataQuery = gql`
query PoolData(
$pool: ID!
$poolAsString: String!
$owner: String!
$user: String
) {
poolData: pool(id: $pool) {
id
totalShares
poolFee
@ -310,16 +315,18 @@ const poolLiquidityQuery = gql`
shares
}
}
}
`
const userPoolShareQuery = gql`
query PoolShare($pool: ID!, $user: String!) {
pool(id: $pool) {
poolDataUser: pool(id: $pool) {
shares(where: { user: $user }) {
shares
}
}
poolSnapshots(first: 1000, where: { pool: $poolAsString }, orderBy: date) {
date
spotPrice
baseTokenLiquidity
datatokenLiquidity
swapVolume
}
}
`
@ -822,33 +829,22 @@ export async function getTopAssetsPublishers(
export async function getPoolData(
chainId: number,
pool: string,
owner: string
owner: string,
user: string
) {
const queryVariables = {
// Using `pool` & `poolAsString` is a workaround to make the mega query work.
// See https://github.com/oceanprotocol/ocean-subgraph/issues/301
pool: pool.toLowerCase(),
owner: owner.toLowerCase()
poolAsString: pool.toLowerCase(),
owner: owner.toLowerCase(),
user: user.toLowerCase()
}
const response: OperationResult<PoolLiquidity> = await fetchData(
poolLiquidityQuery,
queryVariables,
getQueryContext(chainId)
)
return response?.data?.pool
}
export async function getUserPoolShareBalance(
chainId: number,
pool: string,
accountId: string
): Promise<string> {
const queryVariables = {
pool: pool.toLowerCase(),
user: accountId.toLowerCase()
}
const response: OperationResult<PoolLiquidity> = await fetchData(
userPoolShareQuery,
const response: OperationResult<PoolData> = await fetchData(
poolDataQuery,
queryVariables,
getQueryContext(chainId)
)
return response?.data?.pool?.shares[0]?.shares || '0'
return response?.data
}

View File

@ -30,10 +30,12 @@ const txHistoryQueryByPool = gql`
symbol
address
}
baseTokenValue
datatoken {
symbol
address
}
datatokenValue
type
tx
timestamp

View File

@ -1,241 +0,0 @@
/* eslint-disable camelcase */
import React, {
ChangeEvent,
ReactElement,
useCallback,
useEffect,
useState
} from 'react'
import { Line, defaults } from 'react-chartjs-2'
import {
ChartData,
ChartDataSets,
ChartOptions,
ChartTooltipItem,
ChartTooltipOptions
} from 'chart.js'
import Loader from '@shared/atoms/Loader'
import { formatPrice } from '@shared/Price/PriceUnit'
import { useUserPreferences } from '@context/UserPreferences'
import useDarkMode from 'use-dark-mode'
import { darkModeConfig } from '../../../../../app.config'
import Button from '@shared/atoms/Button'
import { LoggerInstance } from '@oceanprotocol/lib'
import { useAsset } from '@context/Asset'
import { gql, OperationResult } from 'urql'
import { PoolHistory } from '../../../../@types/subgraph/PoolHistory'
import { fetchData, getQueryContext } from '@utils/subgraph'
import styles from './Graph.module.css'
declare type GraphType = 'liquidity' | 'price'
// Chart.js global defaults
defaults.global.defaultFontFamily = `'Sharp Sans', -apple-system, BlinkMacSystemFont,
'Segoe UI', Helvetica, Arial, sans-serif`
defaults.global.animation = { easing: 'easeInOutQuart', duration: 1000 }
const REFETCH_INTERVAL = 10000
const lineStyle: Partial<ChartDataSets> = {
fill: false,
lineTension: 0.1,
borderWidth: 2,
pointBorderWidth: 0,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBorderWidth: 0,
pointHitRadius: 2,
pointHoverBackgroundColor: '#ff4092'
}
const tooltipOptions: Partial<ChartTooltipOptions> = {
intersect: false,
titleFontStyle: 'normal',
titleFontSize: 10,
bodyFontSize: 12,
bodyFontStyle: 'bold',
displayColors: false,
xPadding: 10,
yPadding: 10,
cornerRadius: 3,
borderWidth: 1,
caretSize: 7
}
function getOptions(locale: string, isDarkMode: boolean): ChartOptions {
return {
layout: {
padding: {
left: 0,
right: 0,
top: 0,
bottom: 10
}
},
tooltips: {
...tooltipOptions,
backgroundColor: isDarkMode ? `#141414` : `#fff`,
titleFontColor: isDarkMode ? `#e2e2e2` : `#303030`,
bodyFontColor: isDarkMode ? `#fff` : `#141414`,
borderColor: isDarkMode ? `#41474e` : `#e2e2e2`,
callbacks: {
label: (tooltipItem: ChartTooltipItem) =>
`${formatPrice(`${tooltipItem.yLabel}`, locale)} OCEAN`
}
},
legend: {
display: false
},
hover: {
intersect: false,
animationDuration: 0
},
scales: {
yAxes: [
{
display: false
// gridLines: {
// drawBorder: false,
// color: isDarkMode ? '#303030' : '#e2e2e2',
// zeroLineColor: isDarkMode ? '#303030' : '#e2e2e2'
// },
// ticks: { display: false }
}
],
xAxes: [{ display: false, gridLines: { display: true } }]
}
}
}
const graphTypes = ['Liquidity', 'Price']
const poolHistoryQuery = gql`
query PoolHistory($id: String!) {
poolSnapshots(first: 1000, where: { pool: $id }, orderBy: date) {
date
spotPrice
baseTokenLiquidity
datatokenLiquidity
}
}
`
export default function Graph(): ReactElement {
const { locale } = useUserPreferences()
const { price, ddo } = useAsset()
const darkMode = useDarkMode(false, darkModeConfig)
const [options, setOptions] = useState<ChartOptions>()
const [graphType, setGraphType] = useState<GraphType>('liquidity')
const [error, setError] = useState<Error>()
const [isLoading, setIsLoading] = useState(true)
const [dataHistory, setDataHistory] = useState<PoolHistory>()
const [graphData, setGraphData] = useState<ChartData>()
const [graphFetchInterval, setGraphFetchInterval] = useState<NodeJS.Timeout>()
const getPoolHistory = useCallback(async () => {
try {
const queryResult: OperationResult<PoolHistory> = await fetchData(
poolHistoryQuery,
{ id: price.address.toLowerCase() },
getQueryContext(ddo.chainId)
)
setDataHistory(queryResult?.data)
} catch (error) {
console.error('Error fetchData: ', error.message)
setError(error)
}
}, [ddo?.chainId, price?.address])
const refetchGraph = useCallback(async () => {
if (graphFetchInterval) return
const newInterval = setInterval(() => getPoolHistory(), REFETCH_INTERVAL)
setGraphFetchInterval(newInterval)
}, [getPoolHistory, graphFetchInterval])
useEffect(() => {
LoggerInstance.log('Fired GraphOptions!')
const options = getOptions(locale, darkMode.value)
setOptions(options)
}, [locale, darkMode.value])
useEffect(() => {
async function init() {
if (!dataHistory) {
await getPoolHistory()
return
}
LoggerInstance.log('Fired GraphData!')
const latestTimestamps = [
...dataHistory.poolSnapshots.map((item) => {
const date = new Date(item.date * 1000)
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
})
]
const latestLiquidityHistory = [
...dataHistory.poolSnapshots.map((item) => item.baseTokenLiquidity)
]
const latestPriceHistory = [
...dataHistory.poolSnapshots.map((item) => item.datatokenLiquidity)
]
setGraphData({
labels: latestTimestamps.slice(0),
datasets: [
{
...lineStyle,
label: 'Liquidity (OCEAN)',
data:
graphType === 'liquidity'
? latestLiquidityHistory.slice(0)
: latestPriceHistory.slice(0),
borderColor: `#8b98a9`,
pointBackgroundColor: `#8b98a9`
}
]
})
setIsLoading(false)
refetchGraph()
}
init()
return () => clearInterval(graphFetchInterval)
}, [dataHistory, graphType, graphFetchInterval, getPoolHistory, refetchGraph])
function handleGraphTypeSwitch(e: ChangeEvent<HTMLButtonElement>) {
e.preventDefault()
setGraphType(e.currentTarget.textContent.toLowerCase() as GraphType)
}
return (
<div className={styles.graphWrap}>
{isLoading ? (
<Loader />
) : error ? (
<small>{error.message}</small>
) : (
<>
<nav className={styles.type}>
{graphTypes.map((type: GraphType) => (
<Button
key={type}
style="text"
size="small"
onClick={handleGraphTypeSwitch}
className={`${styles.button} ${
graphType === type.toLowerCase() ? styles.active : null
}`}
>
{type}
</Button>
))}
</nav>
<Line height={70} data={graphData} options={options} />
</>
)}
</div>
)
}

View File

@ -1,24 +1,3 @@
.graphWrap {
min-height: 97px;
display: flex;
align-items: center;
justify-content: center;
margin: calc(var(--spacer) / 6) -1.35rem calc(var(--spacer) / 1.5) -1.35rem;
position: relative;
}
@media (min-width: 40rem) {
.graphWrap {
margin-left: -2rem;
margin-right: -2rem;
}
}
.graphWrap canvas {
position: relative;
z-index: 0;
}
.type {
position: absolute;
bottom: -10px;

View File

@ -0,0 +1,40 @@
import Button from '@shared/atoms/Button'
import React, {
ChangeEvent,
Dispatch,
ReactElement,
SetStateAction
} from 'react'
import styles from './Nav.module.css'
import { graphTypes, GraphType } from './_constants'
export default function Nav({
graphType,
setGraphType
}: {
graphType: GraphType
setGraphType: Dispatch<SetStateAction<GraphType>>
}): ReactElement {
function handleGraphTypeSwitch(e: ChangeEvent<HTMLButtonElement>) {
e.preventDefault()
setGraphType(e.currentTarget.textContent.toLowerCase() as GraphType)
}
return (
<nav className={styles.type}>
{graphTypes.map((type: GraphType) => (
<Button
key={type}
style="text"
size="small"
onClick={handleGraphTypeSwitch}
className={`${styles.button} ${
graphType === type.toLowerCase() ? styles.active : null
}`}
>
{type}
</Button>
))}
</nav>
)
}

View File

@ -0,0 +1,67 @@
import {
Chart as ChartJS,
LinearScale,
CategoryScale,
PointElement,
Tooltip,
BarElement,
LineElement,
LineController,
BarController,
ChartDataset,
TooltipOptions,
defaults
} from 'chart.js'
export declare type GraphType = 'liquidity' | 'price' | 'volume'
export const graphTypes = ['Liquidity', 'Price', 'Volume']
// Chart.js global defaults
ChartJS.register(
LineElement,
BarElement,
PointElement,
LinearScale,
CategoryScale,
Tooltip,
LineController,
BarController
)
defaults.font.family = `'Sharp Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif`
defaults.animation = { easing: 'easeInOutQuart', duration: 1000 }
export const lineStyle: Partial<ChartDataset> = {
fill: false,
borderWidth: 2,
pointBorderWidth: 1,
pointRadius: 2,
pointHoverRadius: 4,
pointHoverBorderWidth: 0,
pointHitRadius: 2,
pointHoverBackgroundColor: '#ff4092'
}
export const tooltipOptions: Partial<TooltipOptions> = {
intersect: false,
displayColors: false,
padding: 10,
cornerRadius: 3,
borderWidth: 1,
caretSize: 7,
bodyFont: {
size: 13,
weight: 'bold',
lineHeight: 1,
style: 'normal',
family: defaults.font.family
},
titleFont: {
size: 10,
weight: 'normal',
lineHeight: 1,
style: 'normal',
family: defaults.font.family
}
}

View File

@ -0,0 +1,34 @@
import { formatPrice } from '@shared/Price/PriceUnit'
import { ChartOptions, TooltipItem } from 'chart.js'
import { tooltipOptions } from './_constants'
export function getOptions(locale: string, isDarkMode: boolean): ChartOptions {
return {
layout: {
padding: {
left: 0,
right: 0,
top: 0,
bottom: 20
}
},
plugins: {
tooltip: {
...tooltipOptions,
backgroundColor: isDarkMode ? `#141414` : `#fff`,
titleColor: isDarkMode ? `#e2e2e2` : `#303030`,
bodyColor: isDarkMode ? `#fff` : `#141414`,
borderColor: isDarkMode ? `#41474e` : `#e2e2e2`,
callbacks: {
label: (tooltipItem: TooltipItem<any>) =>
`${formatPrice(`${tooltipItem.formattedValue}`, locale)} OCEAN`
}
}
},
hover: { intersect: false },
scales: {
y: { display: false, beginAtZero: true },
x: { display: false, offset: true }
}
}
}

View File

@ -0,0 +1,20 @@
.graphWrap {
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
margin: calc(var(--spacer) / 6) -1.35rem calc(var(--spacer) / 1.5) -1.35rem;
position: relative;
}
@media (min-width: 40rem) {
.graphWrap {
margin-left: -2rem;
margin-right: -2rem;
}
}
.graphWrap canvas {
position: relative;
z-index: 0;
}

View File

@ -0,0 +1,104 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { ChartData, ChartOptions } from 'chart.js'
import { Bar, Line } from 'react-chartjs-2'
import Loader from '@shared/atoms/Loader'
import { useUserPreferences } from '@context/UserPreferences'
import useDarkMode from 'use-dark-mode'
import { darkModeConfig } from '../../../../../../app.config'
import { LoggerInstance } from '@oceanprotocol/lib'
import styles from './index.module.css'
import Decimal from 'decimal.js'
import { lineStyle, GraphType } from './_constants'
import Nav from './Nav'
import { getOptions } from './_utils'
import { PoolData_poolSnapshots as PoolDataPoolSnapshots } from 'src/@types/subgraph/PoolData'
export default function Graph({
poolSnapshots
}: {
poolSnapshots: PoolDataPoolSnapshots[]
}): ReactElement {
const { locale } = useUserPreferences()
const darkMode = useDarkMode(false, darkModeConfig)
const [options, setOptions] = useState<ChartOptions<any>>()
const [graphType, setGraphType] = useState<GraphType>('liquidity')
const [graphData, setGraphData] = useState<ChartData<any>>()
//
// 0 Get Graph options
//
useEffect(() => {
LoggerInstance.log('[pool graph] Fired getOptions().')
const options = getOptions(locale, darkMode.value)
setOptions(options)
}, [locale, darkMode.value, graphType])
//
// 1 Data manipulation
//
useEffect(() => {
if (!poolSnapshots) return
const timestamps = poolSnapshots.map((item) => {
const date = new Date(item.date * 1000)
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
})
let baseTokenLiquidityCumulative = '0'
const liquidityHistory = poolSnapshots.map((item) => {
baseTokenLiquidityCumulative = new Decimal(baseTokenLiquidityCumulative)
.add(item.baseTokenLiquidity)
.toString()
return baseTokenLiquidityCumulative
})
const priceHistory = poolSnapshots.map((item) => item.spotPrice)
let volumeCumulative = '0'
const volumeHistory = poolSnapshots.map((item) => {
volumeCumulative = new Decimal(volumeCumulative)
.add(item.swapVolume)
.toString()
return volumeCumulative
})
let data
switch (graphType) {
case 'price':
data = priceHistory.slice(0)
break
case 'volume':
data = volumeHistory.slice(0)
break
default:
data = liquidityHistory.slice(0)
break
}
const newGraphData = {
labels: timestamps.slice(0),
datasets: [{ ...lineStyle, data, borderColor: `#8b98a9` }]
}
setGraphData(newGraphData)
LoggerInstance.log('[pool graph] New graph data created:', newGraphData)
}, [poolSnapshots, graphType])
return (
<div className={styles.graphWrap}>
{!graphData ? (
<Loader />
) : (
<>
<Nav graphType={graphType} setGraphType={setGraphType} />
{graphType === 'volume' ? (
<Bar width={416} height={120} data={graphData} options={options} />
) : (
<Line width={416} height={120} data={graphData} options={options} />
)}
</>
)}
</div>
)
}

View File

@ -13,13 +13,16 @@ import TokenList from './TokenList'
import AssetActionHistoryTable from '../AssetActionHistoryTable'
import Graph from './Graph'
import { useAsset } from '@context/Asset'
import { PoolLiquidity_pool as PoolLiquidityData } from '../../../../@types/subgraph/PoolLiquidity'
import { useWeb3 } from '@context/Web3'
import PoolTransactions from '@shared/PoolTransactions'
import { isValidNumber } from '@utils/numbers'
import Decimal from 'decimal.js'
import content from '../../../../../content/price.json'
import { getPoolData, getUserPoolShareBalance } from '@utils/subgraph'
import { getPoolData } from '@utils/subgraph'
import {
PoolData_poolSnapshots as PoolDataPoolSnapshots,
PoolData_poolData as PoolDataPoolData
} from 'src/@types/subgraph/PoolData'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
@ -60,7 +63,7 @@ export default function Pool(): ReactElement {
const { isInPurgatory, ddo, owner, price, refreshInterval, isAssetNetwork } =
useAsset()
const [poolData, setPoolData] = useState<PoolLiquidityData>()
const [poolData, setPoolData] = useState<PoolDataPoolData>()
const [poolInfo, setPoolInfo] = useState<PoolInfo>(
initialPoolInfo as PoolInfo
)
@ -70,6 +73,7 @@ export default function Pool(): ReactElement {
const [poolInfoUser, setPoolInfoUser] = useState<PoolInfoUser>(
initialPoolInfoUser as PoolInfoUser
)
const [poolSnapshots, setPoolSnapshots] = useState<PoolDataPoolSnapshots[]>()
const [hasUserAddedLiquidity, setUserHasAddedLiquidity] = useState(false)
const [showAdd, setShowAdd] = useState(false)
@ -77,34 +81,27 @@ export default function Pool(): ReactElement {
const [isRemoveDisabled, setIsRemoveDisabled] = useState(false)
const [fetchInterval, setFetchInterval] = useState<NodeJS.Timeout>()
const fetchPoolData = useCallback(async () => {
const fetchAllData = useCallback(async () => {
if (!ddo?.chainId || !price?.address || !owner) return
const poolData = await getPoolData(ddo.chainId, price.address, owner)
setPoolData(poolData)
LoggerInstance.log('[pool] Fetched pool data:', poolData)
}, [ddo?.chainId, price?.address, owner])
const fetchUserShares = useCallback(async () => {
if (!ddo?.chainId || !price?.address || !accountId) return
const userShares = await getUserPoolShareBalance(
const response = await getPoolData(
ddo.chainId,
price.address,
accountId
owner,
accountId || ''
)
if (!response) return
setPoolData(response.poolData)
setPoolInfoUser((prevState) => ({
...prevState,
poolShares: userShares
poolShares: response.poolDataUser?.shares[0]?.shares
}))
LoggerInstance.log(`[pool] Fetched user shares: ${userShares}`)
}, [ddo?.chainId, price?.address, accountId])
// Helper: fetch everything
const fetchAllData = useCallback(() => {
fetchPoolData()
fetchUserShares()
}, [fetchPoolData, fetchUserShares])
setPoolSnapshots(response.poolSnapshots)
LoggerInstance.log('[pool] Fetched pool data:', response.poolData)
LoggerInstance.log('[pool] Fetched user data:', response.poolDataUser)
LoggerInstance.log('[pool] Fetched pool snapshots:', response.poolSnapshots)
}, [ddo?.chainId, price?.address, owner, accountId])
// Helper: start interval fetching
const initFetchInterval = useCallback(() => {
@ -443,7 +440,7 @@ export default function Pool(): ReactElement {
{poolInfo?.weightBaseToken}/{poolInfo?.weightDt}
</span>
)}
<Graph />
<Graph poolSnapshots={poolSnapshots} />
</>
}
baseTokenValue={`${price?.ocean}`}