diff --git a/package.json b/package.json index 83b3378217..a29e9ebef3 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,8 @@ "ex": "yarn workspace @beanstalk/examples x", "anvil-arbitrum": "yarn cli:anvil-arbitrum", "anvil-eth-mainnet": "yarn cli:anvil-eth-mainnet", - "anvil": "anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/5ubn94zT7v7DnB5bNW1VOnoIbX5-AG2N --chain-id 1337", - "anvil4tests": "anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/Kk7ktCQL5wz4v4AG8bR2Gun8TAASQ-qi --chain-id 1337 --fork-block-number 18629000" + "anvil": "anvil --fork-url https://arb-mainnet.g.alchemy.com/v2/5ubn94zT7v7DnB5bNW1VOnoIbX5-AG2N --chain-id 1337", + "anvil4tests": "anvil --fork-url https://arb-mainnet.g.alchemy.com/v2/Kk7ktCQL5wz4v4AG8bR2Gun8TAASQ-qi --chain-id 1337 --fork-block-number 18629000" }, "dependencies": { "prettier-plugin-solidity": "1.4.1" diff --git a/projects/sdk/src/lib/BeanstalkSDK.ts b/projects/sdk/src/lib/BeanstalkSDK.ts index f61b9500f1..1e45577325 100644 --- a/projects/sdk/src/lib/BeanstalkSDK.ts +++ b/projects/sdk/src/lib/BeanstalkSDK.ts @@ -135,14 +135,18 @@ export class BeanstalkSDK { } this.signer = config.signer; - if (!config.provider && !config.signer) { + + if (config.signer?.provider){ + this.provider = config.signer.provider as Provider; + } else if (config.provider) { + this.provider = config.provider; + } else { console.log("WARNING: No provider or signer specified, using DefaultProvider."); this.provider = ethers.getDefaultProvider() as Provider; - } else { - this.provider = (config.signer?.provider as Provider) ?? config.provider!; } - this.readProvider = config.readProvider; - this.providerOrSigner = config.signer ?? config.provider!; + + this.readProvider = config.readProvider ?? this.provider; + this.providerOrSigner = this.signer ?? this.provider; this.DEBUG = config.DEBUG ?? false; diff --git a/projects/sdk/src/utils/TestUtils/provider.ts b/projects/sdk/src/utils/TestUtils/provider.ts index 57bbf37526..065b145931 100644 --- a/projects/sdk/src/utils/TestUtils/provider.ts +++ b/projects/sdk/src/utils/TestUtils/provider.ts @@ -12,7 +12,7 @@ export const ACCOUNTS = [ export const getProvider = () => new ethers.providers.StaticJsonRpcProvider(`http://127.0.0.1:8545`, { name: "foundry", - chainId: 41337 // default to arbitrum-local + chainId: 1337 // default to arbitrum-local }); export const setupConnection = (provider: ethers.providers.JsonRpcProvider = getProvider()) => { diff --git a/projects/ui/src/components/Analytics/AdvancedChart.tsx b/projects/ui/src/components/Analytics/AdvancedChart.tsx index a673a78de2..22e72b93c3 100644 --- a/projects/ui/src/components/Analytics/AdvancedChart.tsx +++ b/projects/ui/src/components/Analytics/AdvancedChart.tsx @@ -4,9 +4,13 @@ import { Box, Button, Card, CircularProgress, Drawer } from '@mui/material'; import AddRoundedIcon from '@mui/icons-material/AddRounded'; import CloseIcon from '@mui/icons-material/Close'; import useToggle from '~/hooks/display/useToggle'; -import { apolloClient } from '~/graph/client'; import useSeason from '~/hooks/beanstalk/useSeason'; import { Range, Time } from 'lightweight-charts'; +import { useQueries } from '@tanstack/react-query'; +import { fetchAllSeasonData } from '~/util/Graph'; +import { exists, mayFunctionToValue } from '~/util'; +import { RESEED_SEASON } from '~/constants'; +import useOnAnimationFrame from '~/hooks/display/useOnAnimationFrame'; import ChartV2 from './ChartV2'; import DropdownIcon from '../Common/DropdownIcon'; import SelectDialog from './SelectDialog'; @@ -27,9 +31,11 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => { const season = useSeason(); const chartSetupData = useChartSetupData(); + // wait to mount before fetching data + const ready = useOnAnimationFrame(); + const storedSetting1 = localStorage.getItem('advancedChartTimePeriod'); const storedTimePeriod = storedSetting1 ? JSON.parse(storedSetting1) : undefined; - const storedSetting2 = localStorage.getItem('advancedChartSelectedCharts'); const storedSelectedCharts = storedSetting2 ? JSON.parse(storedSetting2) : undefined; @@ -37,120 +43,88 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => { const [selectedCharts, setSelectedCharts] = useState(storedSelectedCharts || [0]); const [dialogOpen, showDialog, hideDialog] = useToggle(); - const [queryData, setQueryData] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - - useMemo(() => { - async function getSeasonData(getAllData?: boolean) { - const promises: any[] = []; - const output: any[] = []; - const timestamps = new Map(); + const queries = useQueries({ + queries: selectedCharts.map((chartId) => { + const params = chartSetupData[chartId]; + const queryKey = ['analytics', params.id, season.toNumber()]; + return { + queryKey, + queryFn: async () => { + const dataFormatter = params.dataFormatter; + const valueFormatter = params.valueFormatter; + const priceKey = params.priceScaleKey; + const timestamps = new Set(); - const maxRetries = 8 - for (let retries = 0; retries < maxRetries; retries += 1) { - console.debug('[AdvancedChart] Fetching data...'); - try { - for (let i = 0; i < selectedCharts.length; i += 1) { - const chartId = selectedCharts[i]; - const queryConfig = chartSetupData[chartId].queryConfig; - const document = chartSetupData[chartId].document; - const entity = chartSetupData[chartId].documentEntity; + const allSeasonData = await fetchAllSeasonData(params, season.toNumber()); + const output = allSeasonData.map((seasonData) => { + try { + const time = Number(seasonData[params.timeScaleKey]); + const data = dataFormatter ? dataFormatter?.(seasonData): seasonData; + const value = mayFunctionToValue( + valueFormatter(data[priceKey]), + seasonData.season <= RESEED_SEASON - 1 ? 'l1' : 'l2' + ); - const currentSeason = season.toNumber(); + const invalidTime = !exists(time) || timestamps.has(time) || time <= 0; + if (invalidTime || !exists(value)) return undefined; - const iterations = getAllData ? Math.ceil(currentSeason / 1000) + 1 : 1; - for (let j = 0; j < iterations; j += 1) { - const startSeason = getAllData ? currentSeason - j * 1000 : 999999999; - if (startSeason <= 0) continue; - promises.push( - apolloClient - .query({ - ...queryConfig, - query: document, - variables: { - ...queryConfig?.variables, - first: 1000, - season_lte: startSeason, - }, - notifyOnNetworkStatusChange: true, - fetchPolicy: 'no-cache', // Hitting the network every time is MUCH faster than the cache - }) - .then((r) => { - r.data[entity].forEach((seasonData: any) => { - if (seasonData?.season && seasonData.season) { - if (!output[chartId]?.length) { - output[chartId] = []; - } - if (!timestamps.has(seasonData.season)) { - timestamps.set( - seasonData.season, - Number(seasonData[chartSetupData[chartId].timeScaleKey]) - ); - }; - // Some charts will occasionally return two seasons as having the - // same timestamp, here we ensure we only have one datapoint per timestamp - if (timestamps.get(seasonData.season + 1) !== timestamps.get(seasonData.season) - && timestamps.get(seasonData.season - 1) !== timestamps.get(seasonData.season) - ) { - const formattedTime = timestamps.get(seasonData.season); - const dataFormatter = chartSetupData[chartId].dataFormatter; - const _seasonData = dataFormatter ? dataFormatter(seasonData) : seasonData; + timestamps.add(time); - const formattedValue = chartSetupData[ - chartId - ].valueFormatter( - _seasonData[chartSetupData[chartId].priceScaleKey] - ); - if (formattedTime > 0) { - output[chartId][_seasonData.season] = { - time: formattedTime, - value: formattedValue, - customValues: { - season: _seasonData.season - } - }; - }; - }; - }; - }); - }) - ); + return { + time: time as Time, + value, + customValues: { + season: data.season, + }, + } as QueryData; + } catch (e) { + console.debug(`[advancedChart] failed to process some data for ${queryKey}`, e); + return undefined; } - }; - await Promise.all(promises); - output.forEach((dataSet, index) => { - output[index] = dataSet.filter(Boolean); - }); - setQueryData(output); - console.debug('[AdvancedChart] Fetched data successfully!'); - break; - } catch (e) { - console.debug('[AdvancedChart] Failed to fetch data.'); - console.error(e); - if (retries === maxRetries - 1) { - setError(true); - }; - }; + }).filter(Boolean) as QueryData[]; + + // Sort by time + const data = output.sort((a, b) => Number(a.time) - Number(b.time)); + console.debug(`[advancedChart] ${queryKey}`, data); + return data as QueryData[]; + }, + retry: false, + enabled: ready && season.gt(0), + staleTime: Infinity, }; - }; + }), + }); + + const error = useMemo(() => queries.find((a) => !!a.error)?.error, [queries]); - setLoading(true); - getSeasonData(true); - setLoading(false); - }, [chartSetupData, selectedCharts, season]); + const loading = queries.every((q) => q.isLoading) && queries.length > 0; + + const queryData = useMemo( + () => queries.map((q) => q.data).filter(Boolean) as QueryData[][], + [queries] + ); function handleDeselectChart(selectionIndex: number) { const newSelection = [...selectedCharts]; newSelection.splice(selectionIndex, 1); setSelectedCharts(newSelection); - localStorage.setItem('advancedChartSelectedCharts', JSON.stringify(newSelection)); - }; + localStorage.setItem( + 'advancedChartSelectedCharts', + JSON.stringify(newSelection) + ); + } return ( <> - + {!isMobile ? ( = ({ isMobile = false }) => { marginTop: '-1px', transition: 'left 0.3s', borderRadius: 0, - borderLeftColor: 'transparent' + borderLeftColor: 'transparent', }} > = ({ isMobile = false }) => { paddingX: 0.75, }} endIcon={ - + } onClick={() => handleDeselectChart(index)} > @@ -245,7 +221,12 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => { paddingY: 0.25, paddingX: 0.75, }} - endIcon={} + endIcon={ + + } onClick={() => showDialog()} > {selectedCharts.length === 1 @@ -278,7 +259,13 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => { color: 'primary.contrastText', }, }} - endIcon={} + endIcon={ + + } onClick={() => showDialog()} > Add Data @@ -300,8 +287,7 @@ const AdvancedChart: FC<{ isMobile?: boolean }> = ({ isMobile = false }) => { > - ) : - error ? ( + ) : error ? ( = ({ isMobile = false }) => { > Error fetching data - ) : - ( - selectedCharts.length > 0 ? ( - - ) : ( - Click the Add Data button to start charting - ) + ) : selectedCharts.length > 0 ? ( + + ) : ( + + Click the Add Data button to start charting + )} diff --git a/projects/ui/src/components/Analytics/Bean/Crosses.tsx b/projects/ui/src/components/Analytics/Bean/Crosses.tsx deleted file mode 100644 index 43b04c3036..0000000000 --- a/projects/ui/src/components/Analytics/Bean/Crosses.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { tickFormatLocale } from '~/components/Analytics/formatters'; -import { LineChartProps } from '~/components/Common/Charts/LineChart'; -import SeasonPlot, { - SeasonPlotBaseProps, -} from '~/components/Common/Charts/SeasonPlot'; -import { - SeasonalCrossesDocument, - SeasonalCrossesQuery, -} from '~/generated/graphql'; -import useSeason from '~/hooks/beanstalk/useSeason'; -import { SnapshotData } from '~/hooks/beanstalk/useSeasonsQuery'; - -import { FC } from '~/types'; - -const getValue = (season: SnapshotData) => season.crosses; -const formatValue = (value: number) => `${value}`; -const statProps = { - title: 'Peg Crosses', - titleTooltip: 'The total number of times Bean has crossed its peg at the beginning of every Season.', - gap: 0.25, - sx: { ml: 0 }, -}; -const queryConfig = { context: { subgraph: 'bean' } }; -const lineChartProps: Partial = { - yTickFormat: tickFormatLocale, -}; - -const Crosses: FC<{ height?: SeasonPlotBaseProps['height'] }> = ({ - height, -}) => { - const season = useSeason(); - return ( - - height={height} - document={SeasonalCrossesDocument} - defaultSeason={season?.gt(0) ? season.toNumber() : 0} - getValue={getValue} - formatValue={formatValue} - queryConfig={queryConfig} - StatProps={statProps} - LineChartProps={lineChartProps} - dateKey="timestamp" - /> - ); -}; - -export default Crosses; diff --git a/projects/ui/src/components/Analytics/Bean/DeltaBInstant.tsx b/projects/ui/src/components/Analytics/Bean/DeltaBInstant.tsx deleted file mode 100644 index 44de37f6f4..0000000000 --- a/projects/ui/src/components/Analytics/Bean/DeltaBInstant.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { tickFormatLocale } from '~/components/Analytics/formatters'; -import { LineChartProps } from '~/components/Common/Charts/LineChart'; -import SeasonPlot, { - SeasonPlotBaseProps, -} from '~/components/Common/Charts/SeasonPlot'; -import { BEAN } from '~/constants/tokens'; -import { - SeasonalInstantDeltaBDocument, - SeasonalInstantDeltaBQuery, -} from '~/generated/graphql'; -import { SnapshotData } from '~/hooks/beanstalk/useSeasonsQuery'; -import { toTokenUnitsBN } from '~/util'; - -import { FC } from '~/types'; - -const getValue = (season: SnapshotData) => - toTokenUnitsBN(season.instantaneousDeltaB, BEAN[1].decimals).toNumber(); -const formatValue = (value: number) => - `${value.toLocaleString('en-us', { maximumFractionDigits: 2 })}`; -const statProps = { - title: 'Cumulative Instantaneous deltaB', - titleTooltip: - 'The cumulative instantaneous shortage of Beans in liquidity pools on the Minting Whitelist at the beginning of every Season. Pre-exploit values include the instantaneous deltaB in all pools on the Deposit Whitelist.', - gap: 0.25, -}; - -const queryConfig = { - variables: { season_gte: 1 }, - context: { subgraph: 'bean' }, -}; - -const lineChartProps: Partial = { - yTickFormat: tickFormatLocale, - horizontalLineNumber: 0, -}; - -const DeltaBInstant: FC<{ height?: SeasonPlotBaseProps['height'] }> = ({ - height, -}) => ( - - document={SeasonalInstantDeltaBDocument} - height={height} - getValue={getValue} - formatValue={formatValue} - queryConfig={queryConfig} - StatProps={statProps} - LineChartProps={lineChartProps} - dateKey="timestamp" - /> -); - -export default DeltaBInstant; diff --git a/projects/ui/src/components/Analytics/Bean/DeltaBWeighted.tsx b/projects/ui/src/components/Analytics/Bean/DeltaBWeighted.tsx deleted file mode 100644 index 319c6cfaa1..0000000000 --- a/projects/ui/src/components/Analytics/Bean/DeltaBWeighted.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { tickFormatLocale } from '~/components/Analytics/formatters'; -import { LineChartProps } from '~/components/Common/Charts/LineChart'; -import SeasonPlot, { - SeasonPlotBaseProps, -} from '~/components/Common/Charts/SeasonPlot'; -import { BEAN } from '~/constants/tokens'; -import { - SeasonalWeightedDeltaBDocument, - SeasonalWeightedDeltaBQuery, -} from '~/generated/graphql'; -import { SnapshotData } from '~/hooks/beanstalk/useSeasonsQuery'; -import { toTokenUnitsBN } from '~/util'; - -import { FC } from '~/types'; - -const getValue = (season: SnapshotData) => - toTokenUnitsBN(season.twaDeltaB, BEAN[1].decimals).toNumber(); -const formatValue = (value: number) => - `${value.toLocaleString('en-us', { maximumFractionDigits: 2 })}`; -const statProps = { - title: 'Cumulative TWA deltaB', - titleTooltip: - 'The cumulative liquidity and time weighted average shortage of Beans in liquidity pools on the Minting Whitelist at the beginning of every Season. Values during liquidity migrations are omitted. Pre-exploit values include the TWA deltaB in all pools on the Deposit Whitelist.', - gap: 0.25, -}; - -const queryConfig = { - variables: { season_gte: 1 }, - context: { subgraph: 'bean' }, -}; - -const lineChartProps: Partial = { - yTickFormat: tickFormatLocale, - horizontalLineNumber: 0, -}; - -const DeltaBWeighted: FC<{ height?: SeasonPlotBaseProps['height'] }> = ({ - height, -}) => ( - - document={SeasonalWeightedDeltaBDocument} - height={height} - getValue={getValue} - formatValue={formatValue} - queryConfig={queryConfig} - StatProps={statProps} - LineChartProps={lineChartProps} - dateKey="timestamp" - /> -); - -export default DeltaBWeighted; diff --git a/projects/ui/src/components/Analytics/Bean/Liquidity.tsx b/projects/ui/src/components/Analytics/Bean/Liquidity.tsx deleted file mode 100644 index 427a70fc3a..0000000000 --- a/projects/ui/src/components/Analytics/Bean/Liquidity.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import React, { useMemo } from 'react'; -import { - SeasonalLiquidityPerPoolDocument, -} from '~/generated/graphql'; -import useSeason from '~/hooks/beanstalk/useSeason'; -import { FC } from '~/types'; -import useSeasonsQuery, { SeasonRange } from '~/hooks/beanstalk/useSeasonsQuery'; -import { BaseDataPoint, ChartMultiStyles } from '../../Common/Charts/ChartPropProvider'; -import useTimeTabState from '~/hooks/app/useTimeTabState'; -import BaseSeasonPlot, { QueryData } from '../../Common/Charts/BaseSeasonPlot'; -import { SeasonPlotBaseProps } from '~/components/Common/Charts/SeasonPlot'; -import { BEAN_CRV3_LP, BEAN_CRV3_V1_LP, BEAN_ETH_UNIV2_LP, BEAN_ETH_WELL_LP, BEAN_LUSD_LP } from '~/constants/tokens'; -import { BeanstalkPalette } from '../../App/muiTheme'; - -/// Setup SeasonPlot -const formatValue = (value: number) => ( - `$${(value || 0).toLocaleString('en-US', { maximumFractionDigits: 2 })}` -); -const StatProps = { - title: 'Liquidity', - titleTooltip: 'The total USD value of tokens in liquidity pools on the Minting Whitelist at the beginning of every Season. Pre-exploit values include liquidity in pools on the Deposit Whitelist.', - gap: 0.25, - color: 'primary', - sx: { ml: 0 }, -}; - -const Liquidity: FC<{ height?: SeasonPlotBaseProps['height'] }> = ({ - height, -}) => { - - const timeTabParams = useTimeTabState(); - const season = useSeason(); - - const getStatValue = (v?: T[]) => { - if (!v?.length) return 0; - const dataPoint = v[0]; - return dataPoint?.value || 0; - }; - - const BEAN_CRV3 = BEAN_CRV3_LP[1]; - const BEAN_ETH_WELL = BEAN_ETH_WELL_LP[1]; - const BEAN_ETH_UNIV2 = BEAN_ETH_UNIV2_LP[1]; - const BEAN_LUSD_LP_V1 = BEAN_LUSD_LP[1]; - const BEAN_CRV3_V1 = BEAN_CRV3_V1_LP[1]; - - const poolList = [ - BEAN_CRV3, - BEAN_ETH_WELL, - BEAN_ETH_UNIV2, - BEAN_LUSD_LP_V1, - BEAN_CRV3_V1, - ]; - - // Order must be the same as poolList! - const chartStyle: ChartMultiStyles = { - [BEAN_CRV3.address]: { - stroke: BeanstalkPalette.theme.spring.blue, - fillPrimary: BeanstalkPalette.theme.spring.lightBlue - }, - [BEAN_ETH_WELL.address]: { - stroke: BeanstalkPalette.theme.spring.beanstalkGreen, - fillPrimary: BeanstalkPalette.theme.spring.washedGreen - }, - [BEAN_ETH_UNIV2.address]: { - stroke: BeanstalkPalette.theme.spring.chart.purple, - fillPrimary: BeanstalkPalette.theme.spring.chart.purpleLight - }, - [BEAN_LUSD_LP_V1.address]: { - stroke: BeanstalkPalette.theme.spring.grey, - fillPrimary: BeanstalkPalette.theme.spring.lightishGrey - }, - [BEAN_CRV3_V1.address]: { - stroke: BeanstalkPalette.theme.spring.chart.yellow, - fillPrimary: BeanstalkPalette.theme.spring.chart.yellowLight - }, - }; - - // Filters non-relevant tokens from the tooltip on a per-season basis - const seasonFilter = { - [BEAN_ETH_UNIV2.address]: { from: 0, to: 6074 }, - [BEAN_LUSD_LP_V1.address]: { from: 5502, to: 6074 }, - [BEAN_CRV3_V1.address]: { from: 3658, to: 6074 }, - [BEAN_CRV3.address]: { from: 6074, to: Infinity }, - [BEAN_ETH_WELL.address]: { from: 15241, to: Infinity }, - }; - - const queryConfigBeanCrv3 = useMemo(() => ({ - variables: { pool: BEAN_CRV3.address }, - context: { subgraph: 'bean' } - }), [BEAN_CRV3.address]); - - const queryConfigBeanEthWell = useMemo(() => ({ - variables: { pool: BEAN_ETH_WELL.address }, - context: { subgraph: 'bean' } - }), [BEAN_ETH_WELL.address]); - - const queryConfigBeanEthOld = useMemo(() => ({ - variables: { pool: BEAN_ETH_UNIV2.address }, - context: { subgraph: 'bean' } - }), [BEAN_ETH_UNIV2.address]); - - const queryConfigBeanLusdOld = useMemo(() => ({ - variables: { pool: BEAN_LUSD_LP_V1.address }, - context: { subgraph: 'bean' } - }), [BEAN_LUSD_LP_V1.address]); - - const queryConfigBeanCrv3Old = useMemo(() => ({ - variables: { pool: BEAN_CRV3_V1.address }, - context: { subgraph: 'bean' } - }), [BEAN_CRV3_V1.address]); - - const beanCrv3 = useSeasonsQuery(SeasonalLiquidityPerPoolDocument, timeTabParams[0][1], queryConfigBeanCrv3); - const beanEthWell = useSeasonsQuery(SeasonalLiquidityPerPoolDocument, timeTabParams[0][1], queryConfigBeanEthWell); - const beanEthOld = useSeasonsQuery(SeasonalLiquidityPerPoolDocument, SeasonRange.ALL, queryConfigBeanEthOld); - const beanLusdOld = useSeasonsQuery(SeasonalLiquidityPerPoolDocument, SeasonRange.ALL, queryConfigBeanLusdOld); - const beanCrv3Old = useSeasonsQuery(SeasonalLiquidityPerPoolDocument, SeasonRange.ALL, queryConfigBeanCrv3Old); - - let seasonData - if (timeTabParams[0][1] === SeasonRange.ALL) { - seasonData = [ - beanCrv3.data?.seasons, - beanEthWell.data?.seasons, - beanEthOld.data?.seasons, - beanLusdOld.data?.seasons, - beanCrv3Old.data?.seasons - ].flat(Infinity); - } else { - seasonData = [ - beanCrv3.data?.seasons, - beanEthWell.data?.seasons, - ].flat(Infinity); - }; - - const loading = beanCrv3.loading || beanEthWell.loading || beanEthOld.loading || beanLusdOld.loading || beanCrv3Old.loading; - - const processedSeasons: any[] = []; - const defaultDataPoint = { - season: 0, - date: 0, - value: 0, - [BEAN_CRV3.address]: 0, - [BEAN_ETH_WELL.address]: 0, - [BEAN_ETH_UNIV2.address]: 0, - [BEAN_LUSD_LP_V1.address]: 0, - [BEAN_CRV3_V1.address]: 0, - }; - - if (season && !loading && seasonData[0] && seasonData[0].season) { - const latestSeason = seasonData[0].season; - seasonData.forEach((dataPoint) => { - const seasonDiff = latestSeason - dataPoint.season; - if (!processedSeasons[seasonDiff]) { - processedSeasons[seasonDiff] = { ...defaultDataPoint }; - }; - processedSeasons[seasonDiff].season = Number(dataPoint.season); - processedSeasons[seasonDiff].date = new Date(Number(dataPoint.updatedAt) * 1000); - processedSeasons[seasonDiff][dataPoint.id.slice(0, 42)] = Number(dataPoint.liquidityUSD); - processedSeasons[seasonDiff].value += Number(dataPoint.liquidityUSD); - }); - }; - - const queryData: QueryData = { - data: [processedSeasons.filter(Boolean).reverse()], - loading: loading, - keys: poolList.map((pool) => pool.address), - error: undefined, - }; - - return ( - - ); -}; - -export default Liquidity; - diff --git a/projects/ui/src/components/Analytics/Bean/LiquidityPerPool.graphql b/projects/ui/src/components/Analytics/Bean/LiquidityPerPool.graphql index dddf5da286..1c7dafd8f7 100644 --- a/projects/ui/src/components/Analytics/Bean/LiquidityPerPool.graphql +++ b/projects/ui/src/components/Analytics/Bean/LiquidityPerPool.graphql @@ -1,14 +1,6 @@ -query SeasonalLiquidityPerPool( - $first: Int, - $season_lte: Int, - $pool: String! -) { +query SeasonalLiquidityPerPool($first: Int, $season_lte: Int, $pool: String!) { seasons: poolHourlySnapshots( - where: { - season_lte: $season_lte, - season_gt: 0, - pool_starts_with_nocase: $pool - } + where: { season_lte: $season_lte, season_gt: 0, pool: $pool } first: $first orderBy: season orderDirection: desc diff --git a/projects/ui/src/components/Analytics/Bean/LiquiditySupplyRatio.tsx b/projects/ui/src/components/Analytics/Bean/LiquiditySupplyRatio.tsx deleted file mode 100644 index dd9cfa8b38..0000000000 --- a/projects/ui/src/components/Analytics/Bean/LiquiditySupplyRatio.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { tickFormatPercentage } from '~/components/Analytics/formatters'; -import { LineChartProps } from '~/components/Common/Charts/LineChart'; -import SeasonPlot, { - SeasonPlotBaseProps, -} from '~/components/Common/Charts/SeasonPlot'; -import { - LiquiditySupplyRatioDocument, - LiquiditySupplyRatioQuery, -} from '~/generated/graphql'; -import useSeason from '~/hooks/beanstalk/useSeason'; -import { FC } from '~/types'; - -const getValue = (season: LiquiditySupplyRatioQuery['seasons'][number]) => - (season.supplyInPegLP * 100); -const formatValue = (value: number) => - `${value.toFixed(4)}%`; -const statProps = { - title: 'Liquidity to Supply Ratio', - titleTooltip: - `The ratio of Beans in liquidity pools on the Minting Whitelist per Bean, displayed as a percentage, at the beginning of every Season. The Liquidity to Supply Ratio is a useful indicator of Beanstalk's health. Pre-exploit values include liquidity in pools on the Deposit Whitelist.`, - gap: 0.25, -}; -const queryConfig = { - variables: { season_gt: 0 }, - context: { subgraph: 'bean' }, -}; -const lineChartProps: Partial = { - yTickFormat: tickFormatPercentage, -}; - -const LiquiditySupplyRatio: FC<{ height?: SeasonPlotBaseProps['height'] }> = ({ - height, -}) => { - const season = useSeason(); - return ( - - document={LiquiditySupplyRatioDocument} - height={height} - defaultSeason={season?.gt(0) ? season.toNumber() : 0} - getValue={getValue} - formatValue={formatValue} - StatProps={statProps} - LineChartProps={lineChartProps} - queryConfig={queryConfig} - dateKey="timestamp" - /> - ); -}; - -export default LiquiditySupplyRatio; diff --git a/projects/ui/src/components/Analytics/Bean/MarketCap.tsx b/projects/ui/src/components/Analytics/Bean/MarketCap.tsx index 6eb48e5a72..1860a4ee9f 100644 --- a/projects/ui/src/components/Analytics/Bean/MarketCap.tsx +++ b/projects/ui/src/components/Analytics/Bean/MarketCap.tsx @@ -35,6 +35,7 @@ const MarketCap: FC<{ height?: SeasonPlotBaseProps['height'] }> = ({ formatValue={formatValue} StatProps={statProps} LineChartProps={lineChartProps} + name="seasonalMarketCap" /> ); }; diff --git a/projects/ui/src/components/Analytics/Bean/PriceInstant.graphql b/projects/ui/src/components/Analytics/Bean/PriceInstant.graphql index 52d83a813e..1f7cfb4a18 100644 --- a/projects/ui/src/components/Analytics/Bean/PriceInstant.graphql +++ b/projects/ui/src/components/Analytics/Bean/PriceInstant.graphql @@ -1,6 +1,6 @@ -query SeasonalInstantPrice($season_lte: Int, $first: Int, $season_gte: Int) { +query SeasonalInstantPrice($season_lte: Int, $first: Int, $season_gt: Int) { seasons: beanHourlySnapshots( - where: { season_lte: $season_lte, season_gte: $season_gte } + where: { season_lte: $season_lte, season_gt: $season_gt } first: $first orderBy: season orderDirection: desc diff --git a/projects/ui/src/components/Analytics/Bean/PriceInstant.tsx b/projects/ui/src/components/Analytics/Bean/PriceInstant.tsx index 2ac6766db2..1c7c1eabd3 100644 --- a/projects/ui/src/components/Analytics/Bean/PriceInstant.tsx +++ b/projects/ui/src/components/Analytics/Bean/PriceInstant.tsx @@ -14,6 +14,10 @@ import useSeason from '~/hooks/beanstalk/useSeason'; import { SnapshotData } from '~/hooks/beanstalk/useSeasonsQuery'; import { FC } from '~/types'; +import { + subgraphQueryConfigs, + subgraphQueryKeys, +} from '~/util/Graph'; const getValue = (season: SnapshotData) => parseFloat(season.price); @@ -24,11 +28,6 @@ const statProps = { gap: 0.25, }; -const queryConfig = { - variables: { season_gte: 1 }, - context: { subgraph: 'bean' }, -}; - const lineChartProps: Partial = { pegLine: true, yTickFormat: tickFormatBeanPrice, @@ -47,10 +46,11 @@ const Price: FC<{ height?: SeasonPlotBaseProps['height'] }> = ({ height }) => { defaultSeason={season?.gt(0) ? season.toNumber() : 0} getValue={getValue} formatValue={formatValue} - queryConfig={queryConfig} + queryConfig={subgraphQueryConfigs.priceInstantBEAN.queryOptions} StatProps={statProps} LineChartProps={lineChartProps} dateKey="timestamp" + name={subgraphQueryKeys.priceInstantBEAN} /> ); }; diff --git a/projects/ui/src/components/Analytics/Bean/PriceWeighted.tsx b/projects/ui/src/components/Analytics/Bean/PriceWeighted.tsx deleted file mode 100644 index f43ad6a582..0000000000 --- a/projects/ui/src/components/Analytics/Bean/PriceWeighted.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { tickFormatBeanPrice } from '~/components/Analytics/formatters'; -import { LineChartProps } from '~/components/Common/Charts/LineChart'; -import SeasonPlot, { - SeasonPlotBaseProps, -} from '~/components/Common/Charts/SeasonPlot'; -import { - SeasonalWeightedPriceDocument, - SeasonalWeightedPriceQuery, -} from '~/generated/graphql'; -import { SnapshotData } from '~/hooks/beanstalk/useSeasonsQuery'; - -import { FC } from '~/types'; - -const getValue = (season: SnapshotData) => - parseFloat(season.twaPrice); -const formatValue = (value: number) => `$${value.toFixed(4)}`; -const statProps = { - title: 'TWA Bean Price', - titleTooltip: - 'The cumulative liquidity and time weighted average USD price of 1 Bean at the beginning of every Season. Values during liquidity migrations are omitted. Pre-exploit values include the TWA price in all pools on the Deposit Whitelist.', - gap: 0.25, -}; - -const queryConfig = { - variables: { season_gte: 1 }, - context: { subgraph: 'bean' }, -}; - -const lineChartProps: Partial = { - pegLine: true, - yTickFormat: tickFormatBeanPrice, -}; - -const PriceWeighted: FC<{ height?: SeasonPlotBaseProps['height'] }> = ({ - height, -}) => ( - - document={SeasonalWeightedPriceDocument} - height={height} - getValue={getValue} - formatValue={formatValue} - queryConfig={queryConfig} - StatProps={statProps} - LineChartProps={lineChartProps} - dateKey="timestamp" - /> -); - -export default PriceWeighted; diff --git a/projects/ui/src/components/Analytics/Bean/Supply.tsx b/projects/ui/src/components/Analytics/Bean/Supply.tsx deleted file mode 100644 index b98f2f4693..0000000000 --- a/projects/ui/src/components/Analytics/Bean/Supply.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import SeasonPlot, { - SeasonPlotBaseProps, -} from '~/components/Common/Charts/SeasonPlot'; -import { - SeasonalSupplyQuery, - SeasonalSupplyDocument, -} from '~/generated/graphql'; -import { BEAN } from '~/constants/tokens'; -import { toTokenUnitsBN } from '~/util'; -import { SnapshotData } from '~/hooks/beanstalk/useSeasonsQuery'; -import { LineChartProps } from '~/components/Common/Charts/LineChart'; -import { tickFormatTruncated } from '~/components/Analytics/formatters'; - -import { FC } from '~/types'; - -const getValue = (season: SnapshotData) => - toTokenUnitsBN(season.beans, BEAN[1].decimals).toNumber(); -const formatValue = (value: number) => - `${value.toLocaleString('en-US', { maximumFractionDigits: 0 })}`; -const useStatProps = () => { - const theme = useTheme(); - const isTiny = useMediaQuery(theme.breakpoints.down('md')); - return { - title: isTiny ? 'Supply' : 'Bean Supply', - titleTooltip: 'The total Bean supply at the beginning of every Season.', - gap: 0.25, - }; -}; -const lineChartProps: Partial = { - yTickFormat: tickFormatTruncated, -}; - -const Supply: FC<{ height?: SeasonPlotBaseProps['height'] }> = ({ height }) => { - const statProps = useStatProps(); - return ( - - height={height} - document={SeasonalSupplyDocument} - getValue={getValue} - formatValue={formatValue} - LineChartProps={lineChartProps} - StatProps={statProps} - /> - ); -}; - -export default Supply; diff --git a/projects/ui/src/components/Analytics/Bean/Volume.tsx b/projects/ui/src/components/Analytics/Bean/Volume.tsx deleted file mode 100644 index a22f959fed..0000000000 --- a/projects/ui/src/components/Analytics/Bean/Volume.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { tickFormatUSD } from '~/components/Analytics/formatters'; -import { CURVES, LineChartProps } from '~/components/Common/Charts/LineChart'; -import SeasonPlot, { - SeasonPlotBaseProps, -} from '~/components/Common/Charts/SeasonPlot'; -import { - SeasonalVolumeDocument, - SeasonalVolumeQuery, -} from '~/generated/graphql'; -import useSeason from '~/hooks/beanstalk/useSeason'; - -import { FC } from '~/types'; - -const getValue = (season: SeasonalVolumeQuery['seasons'][number]) => - parseFloat(season.deltaVolumeUSD); -const formatValue = (value: number) => - `$${value.toLocaleString('en-US', { maximumFractionDigits: 0 })}`; -const statProps = { - title: 'Volume', - titleTooltip: 'The total USD volume in liquidity pools on the Minting Whitelist every Season.', - gap: 0.25, -}; -const queryConfig = { context: { subgraph: 'bean' } }; -const lineChartProps: Partial = { - curve: CURVES.step, - yTickFormat: tickFormatUSD, -}; - -const Volume: FC<{ height?: SeasonPlotBaseProps['height'] }> = ({ height }) => { - const season = useSeason(); - return ( - - ); -}; - -export default Volume; diff --git a/projects/ui/src/components/Analytics/Bean/VolumeChart.tsx b/projects/ui/src/components/Analytics/Bean/VolumeChart.tsx deleted file mode 100644 index b61c041f18..0000000000 --- a/projects/ui/src/components/Analytics/Bean/VolumeChart.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { CircularProgress, Stack, Typography } from '@mui/material'; -import React, { useMemo, useState } from 'react'; -import { timeFormat, timeParse } from 'd3-time-format'; -import ParentSize from '@visx/responsive/lib/components/ParentSize'; -import { - SeasonalVolumeDocument, - SeasonalVolumeQuery, -} from '~/generated/graphql'; - -import BarChart from '~/components/Common/Charts/BarChart'; -import { BaseDataPoint } from '../../Common/Charts/ChartPropProvider'; -import ChartInfoOverlay from '../../Common/Charts/ChartInfoOverlay'; -import { FC } from '~/types'; -import { QueryData } from '~/components/Common/Charts/BaseSeasonPlot'; -import QueryState from '../../Common/Charts/QueryState'; -import Row from '../../Common/Row'; -import TimeTabs from '../../Common/Charts/TimeTabs'; -import { tickFormatUSD } from '~/components/Analytics/formatters'; -import useGenerateChartSeries from '~/hooks/beanstalk/useGenerateChartSeries'; -import useSeasonsQuery from '~/hooks/beanstalk/useSeasonsQuery'; -import useTimeTabState from '~/hooks/app/useTimeTabState'; - -type BarChartDatum = { - count: number; - maxSeason: number; - minSeason: number; - date: Date; -}; - -type DataByDate = { - [key: string]: BaseDataPoint[]; -}; - -const VolumeChart: FC<{ width?: number; height: number }> = ({ - width = undefined, - height, -}) => { - const [currentHoverBar, setHoverBar] = useState( - undefined - ); - - const queryConfig = useMemo(() => ({ context: { subgraph: 'bean' } }), []); - - const timeTabParams = useTimeTabState(); - - const seasonsQuery = useSeasonsQuery( - SeasonalVolumeDocument, - timeTabParams[0][1], - queryConfig - ); - - const getValue = (season: SeasonalVolumeQuery['seasons'][number]) => - parseFloat(season.deltaVolumeUSD); - - const queryData: QueryData = useGenerateChartSeries( - [{ query: seasonsQuery, getValue, key: 'value' }], - timeTabParams[0], - 'timestamp' - ); - - const transformData: (data: BaseDataPoint[]) => BarChartDatum[] = (data) => { - if (data?.length === 0) return []; - - const dateFormat = timeFormat('%Y/%m/%d'); - const parseDate = timeParse('%Y/%m/%d'); - const dataByDate = data.reduce((accum: DataByDate, datum: any) => { - const key = dateFormat(datum.date); - if (!accum[key]) { - accum[key] = []; - } - accum[key].push(datum); - return accum; - }, {}); - - return Object.entries(dataByDate).map(([date, dayData]) => { - const seasons = dayData.map((datum) => datum.season); - return { - date: parseDate(date) as Date, - maxSeason: Math.max(...seasons), - minSeason: Math.min(...seasons), - count: dayData.reduce((accum: number, datum) => accum + datum.value, 0), - }; - }); - }; - - const formatValue = (value: number) => - `$${value.toLocaleString('en-US', { maximumFractionDigits: 0 })}`; - - const currentSeason = - currentHoverBar?.minSeason && currentHoverBar?.maxSeason - ? `${currentHoverBar?.minSeason ?? ''} - ${ - currentHoverBar?.maxSeason ?? '' - }` - : 0; - - const currentDate = currentHoverBar ? currentHoverBar.date.toLocaleDateString() : (new Date()).toLocaleDateString(); - - const chartControlsHeight = 75; - const chartHeight = height - chartControlsHeight; - const containerStyle = { - height: '100%', - alignItems: 'center', - justifyContent: 'center', - minHeight: chartHeight, - }; - - return ( - <> - - - - - - - - - - } - error={ - - An error occurred while loading this data. - - } - success={ - - {(parent) => ( - datum.date} - getY={(datum) => Number(datum.count)} - xTickFormat={(date: Date) => date.toLocaleDateString(undefined, { month: '2-digit', day: '2-digit' })} - yTickFormat={tickFormatUSD} - width={width || parent.width} - height={chartHeight || parent.height} - onBarHoverEnter={(datum) => { - setHoverBar(datum as BarChartDatum); - }} - /> - )} - - } - /> - - ); -}; - -export default VolumeChart; diff --git a/projects/ui/src/components/Analytics/Bean/index.tsx b/projects/ui/src/components/Analytics/Bean/index.tsx deleted file mode 100644 index f4c5d32211..0000000000 --- a/projects/ui/src/components/Analytics/Bean/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Card, Tab, Tabs } from '@mui/material'; - -import Crosses from '~/components/Analytics/Bean/Crosses'; -import DeltaBInstant from '~/components/Analytics/Bean/DeltaBInstant'; -import DeltaBWeighted from '~/components/Analytics/Bean/DeltaBWeighted'; -import { FC } from '~/types'; -import Liquidity from '~/components/Analytics/Bean/Liquidity'; -import MarketCap from '~/components/Analytics/Bean/MarketCap'; -import React from 'react'; -import Supply from '~/components/Analytics/Bean/Supply'; -import VolumeChart from '~/components/Analytics/Bean/VolumeChart'; -import useTabs from '~/hooks/display/useTabs'; -import Price from './PriceInstant'; -import LiquiditySupplyRatio from './LiquiditySupplyRatio'; -import PriceWeighted from './PriceWeighted'; - -const SLUGS = [ - 'price', - 'volume', - 'liquidity', - 'mktcap', - 'supply', - 'crosses', - 'inst_delta_b', - 'twa_delta_b', - 'liquiditysupplyratio', -]; - -const BeanAnalytics: FC<{}> = () => { - const [tab, handleChangeTab] = useTabs(SLUGS, 'bean'); - const CHART_HEIGHT = 300; - return ( - - - - - - - - - - - - - - {/* - TODO: The height prop currently *only* reflects in the chart height. However, the full component - has other components that yield a larger height. All the components below should be refactored - to account for their additional parts, so when a height is put in then you would get that - exact height. Alternatively, the existing height prop should be renamed to chartHeight. - */} - {tab === 0 && } - {tab === 1 && } - {tab === 2 && } - {tab === 3 && } - {tab === 4 && } - {tab === 5 && } - {tab === 6 && } - {tab === 7 && } - {tab === 8 && } - {tab === 9 && } - - ); -}; - -export default BeanAnalytics; diff --git a/projects/ui/src/components/Analytics/ChartV2.tsx b/projects/ui/src/components/Analytics/ChartV2.tsx index 4148a86e4d..cf9b7624af 100644 --- a/projects/ui/src/components/Analytics/ChartV2.tsx +++ b/projects/ui/src/components/Analytics/ChartV2.tsx @@ -60,6 +60,47 @@ export type ChartV2DataProps = { selected: number[]; }; +function getTimezoneCorrectedTime(utcTime: Date, tickMarkType: TickMarkType) { + let timestamp + if (utcTime instanceof Date) { + timestamp = utcTime.getTime() / 1000 + } else { + timestamp = utcTime + }; + const correctedTime = new Date((timestamp * 1000)); + let options = {}; + switch(tickMarkType) { + case TickMarkType.Year: + options = { + year: 'numeric' + } + break + case TickMarkType.Month: + options = { + month: 'short' + } + break + case TickMarkType.DayOfMonth: + options = { + day: '2-digit' + } + break + case TickMarkType.Time: + options = { + hour: '2-digit', + minute: '2-digit' + } + break + default: + options = { + hour: '2-digit', + minute: '2-digit', + seconds: '2-digit' + }; + }; + return correctedTime.toLocaleString('en-GB', options); +}; + const ChartV2: FC = ({ formattedData, drawPegLine, @@ -77,47 +118,6 @@ const ChartV2: FC = ({ const [firstDataPoint, setFirstDataPoint] = useState(); const [dataPoint, setDataPoint] = useState(); - function getTimezoneCorrectedTime(utcTime: Date, tickMarkType: TickMarkType) { - let timestamp - if (utcTime instanceof Date) { - timestamp = utcTime.getTime() / 1000 - } else { - timestamp = utcTime - }; - const correctedTime = new Date((timestamp * 1000)); - let options = {}; - switch(tickMarkType) { - case TickMarkType.Year: - options = { - year: 'numeric' - } - break - case TickMarkType.Month: - options = { - month: 'short' - } - break - case TickMarkType.DayOfMonth: - options = { - day: '2-digit' - } - break - case TickMarkType.Time: - options = { - hour: '2-digit', - minute: '2-digit' - } - break - default: - options = { - hour: '2-digit', - minute: '2-digit', - seconds: '2-digit' - }; - }; - return correctedTime.toLocaleString('en-GB', options); - }; - // Menu const [leftAnchorEl, setLeftAnchorEl] = useState(null); const [rightAnchorEl, setRightAnchorEl] = useState(null); @@ -322,13 +322,12 @@ const ChartV2: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [timePeriod]); - useEffect(() => { + useEffect(() => { if (!chart.current || !formattedData) return; - const numberOfCharts = selected?.length || 0; for (let i = 0; i < numberOfCharts; i += 1) { - if (!formattedData[selected[i]]) return; - areaSeries.current[i].setData(formattedData[selected[i]]); + if (!formattedData[i]) return; + areaSeries.current[i].setData(formattedData[i]); }; const storedSetting = localStorage.getItem('advancedChartTimePeriod'); @@ -342,13 +341,12 @@ const ChartV2: FC = ({ let _time = 0; const _value: number[] = []; let _season = 0; - - selected.forEach((selection) => { - const selectedData = formattedData[selection]; + selected.forEach((_, i) => { + const selectedData = formattedData[i]; const dataIndex = mode === 'last' ? selectedData.length - 1 : 0; - _time = Math.max(_time, selectedData[dataIndex].time.valueOf() as number); - _season = Math.max(_season, selectedData[dataIndex].customValues.season); - _value.push(selectedData[dataIndex].value); + _time = Math.max(_time, selectedData[dataIndex]?.time.valueOf() as number); + _season = Math.max(_season, selectedData[dataIndex]?.customValues?.season); + _value.push(selectedData[dataIndex]?.value); }); return { @@ -371,7 +369,7 @@ const ChartV2: FC = ({ const seriesValueAfter = series.dataByIndex(param.logical?.valueOf() as number, 1); // @ts-ignore hoveredValues.push(seriesValueBefore && seriesValueAfter ? seriesValueBefore?.value : 0); - hoveredSeason = Math.max(hoveredSeason, (seriesValueBefore?.customValues!.season as number || 0)); + hoveredSeason = Math.max(hoveredSeason, (seriesValueBefore?.customValues?.season as number || 0)); }); if (!param.time) { setDataPoint(undefined); @@ -388,8 +386,8 @@ const ChartV2: FC = ({ function timeRangeChangeHandler(param: Range