11import { Box , Checkbox , FormControlLabel , Typography , useTheme } from '@mui/material' ;
2- import useHistoricalRates from 'hooks/useHistoricalRates ' ;
3- import React , { FC , useCallback , useMemo , useState } from 'react' ;
2+ import { useQuery } from '@tanstack/react-query ' ;
3+ import React , { FC , useCallback , useEffect , useMemo , useState } from 'react' ;
44import { useTranslation } from 'react-i18next' ;
55import { CartesianGrid , Line , LineChart , ResponsiveContainer , Tooltip , XAxis , YAxis } from 'recharts' ;
6+ import { formatEther } from 'viem' ;
7+ import { usePublicClient } from 'wagmi' ;
8+ import { floatingDepositRates , floatingUtilization } from '@exactly/lib' ;
9+
10+ import { ratePreviewerAbi , ratePreviewerAddress , ratePreviewerCode } from 'generated/wagmi' ;
11+ import useAccountData from 'hooks/useAccountData' ;
12+ import { useGlobalError } from 'contexts/GlobalErrorContext' ;
13+ import { defaultChain } from 'utils/client' ;
614import { toPercentage } from 'utils/utils' ;
15+ import { track } from 'utils/mixpanel' ;
716import ButtonsChart from '../ButtonsChart' ;
817import LoadingChart from '../LoadingChart' ;
918import TooltipChart from '../TooltipChart' ;
10- import { track } from 'utils/mixpanel' ;
1119
1220type Props = {
1321 symbol : string ;
1422} ;
1523
24+ type Range = '1W' | '1M' | '3M' ;
25+
26+ const WINDOW_SECONDS : Record < Range , number > = { '1W' : 7 * 86_400 , '1M' : 30 * 86_400 , '3M' : 90 * 86_400 } ;
27+ const SECONDS_PER_BLOCK : Record < number , number > = { 1 : 12 , 10 : 2 , 8453 : 2 , 11_155_420 : 2 , 84_532 : 2 } ;
28+ const POINTS = 30 ;
29+ const CONCURRENCY = 8 ;
30+
1631const HistoricalRateChart : FC < Props > = ( { symbol } ) => {
1732 const { t } = useTranslation ( ) ;
18- const [ showUtilization , setShowUtilization ] = useState ( false ) ;
1933 const { palette } = useTheme ( ) ;
20- const { loading, rates, getRates } = useHistoricalRates ( symbol ) ;
34+ const { setIndexerError } = useGlobalError ( ) ;
35+ const [ showUtilization , setShowUtilization ] = useState ( false ) ;
36+ const [ range , setRange ] = useState < Range > ( '1W' ) ;
37+
38+ const { marketAccount } = useAccountData ( symbol ) ;
39+ const market = marketAccount ?. market ;
40+ const client = usePublicClient ( { chainId : defaultChain . id } ) ;
41+ const ratePreviewer = ratePreviewerAddress [ defaultChain . id as keyof typeof ratePreviewerAddress ] ;
42+ const code = ratePreviewerCode [ defaultChain . id as keyof typeof ratePreviewerCode ] ;
2143
22- const sortedRates = useMemo ( ( ) => rates . sort ( ( a , b ) => ( a . date > b . date ? 1 : - 1 ) ) , [ rates ] ) ;
44+ const {
45+ data : points = [ ] ,
46+ isLoading,
47+ isError,
48+ } = useQuery ( {
49+ queryKey : [ 'historicalRates' , defaultChain . id , market , range ] ,
50+ enabled : Boolean ( client && market && ratePreviewer && code ) ,
51+ staleTime : 60_000 ,
52+ queryFn : async ( ) => {
53+ if ( ! client || ! market || ! ratePreviewer || ! code ) return [ ] ;
54+
55+ const blockTime = SECONDS_PER_BLOCK [ defaultChain . id ] ?? 2 ;
56+ const latest = await client . getBlockNumber ( ) ;
57+ const anchor = Number ( ( await client . getBlock ( { blockNumber : latest } ) ) . timestamp ) ;
58+ const span = BigInt ( Math . ceil ( WINDOW_SECONDS [ range ] / blockTime ) ) ;
59+ const from = latest > span ? latest - span : 1n ;
60+ const width = latest - from ;
61+ const count = width < BigInt ( POINTS ) ? Number ( width ) + 1 : POINTS ;
62+ const blocks = [
63+ ...new Set (
64+ Array . from ( { length : count } , ( _ , i ) => from + ( width * BigInt ( i ) ) / BigInt ( Math . max ( count - 1 , 1 ) ) ) ,
65+ ) ,
66+ ] ;
67+
68+ const series : { date : number ; depositApr : number ; borrowApr : number ; utilization : number } [ ] = [ ] ;
69+ for ( let i = 0 ; i < blocks . length ; i += CONCURRENCY ) {
70+ const batch = await Promise . allSettled (
71+ blocks . slice ( i , i + CONCURRENCY ) . map ( async ( blockNumber ) => {
72+ const snapshot = await client . readContract ( {
73+ address : ratePreviewer ,
74+ abi : ratePreviewerAbi ,
75+ functionName : 'snapshot' ,
76+ blockNumber,
77+ stateOverride : [ { address : ratePreviewer , code } ] ,
78+ } ) ;
79+ const state = snapshot . find ( ( s ) => s . market . toLowerCase ( ) === market . toLowerCase ( ) ) ;
80+ if ( ! state ) return undefined ;
81+ const timestamp = anchor - Number ( latest - blockNumber ) * blockTime ;
82+ const deposit = floatingDepositRates (
83+ [
84+ {
85+ ...state ,
86+ lastFloatingDebtUpdate : Number ( state . lastFloatingDebtUpdate ) ,
87+ lastAccumulatorAccrual : Number ( state . lastAccumulatorAccrual ) ,
88+ maxFuturePools : Number ( state . maxFuturePools ) ,
89+ } ,
90+ ] ,
91+ timestamp ,
92+ ) [ 0 ] ;
93+ return {
94+ date : timestamp * 1_000 ,
95+ depositApr : deposit ? Number ( formatEther ( deposit . rate ) ) : 0 ,
96+ borrowApr : Number ( formatEther ( state . floatingRate ) ) ,
97+ utilization : state . floatingAssets
98+ ? Number ( formatEther ( floatingUtilization ( state . floatingAssets , state . floatingDebt ) ) )
99+ : 0 ,
100+ } ;
101+ } ) ,
102+ ) ;
103+ for ( const result of batch ) if ( result . status === 'fulfilled' && result . value ) series . push ( result . value ) ;
104+ }
105+
106+ return series . sort ( ( a , b ) => a . date - b . date ) ;
107+ } ,
108+ } ) ;
109+
110+ const loading = isLoading || ! marketAccount ;
111+
112+ useEffect ( ( ) => {
113+ if ( isError ) setIndexerError ( ) ;
114+ } , [ isError , setIndexerError ] ) ;
23115
24116 const buttons = useMemo (
25117 ( ) => [
26- {
27- label : t ( '1W' ) ,
28- onClick : ( ) => getRates ( 30 , 3_600 * 6 ) ,
29- } ,
30- {
31- label : t ( '1M' ) ,
32- onClick : ( ) => getRates ( 30 , 3_600 * 24 ) ,
33- } ,
34- {
35- label : t ( '3M' ) ,
36- onClick : ( ) => getRates ( 90 , 3_600 * 24 ) ,
37- } ,
118+ { label : t ( '1W' ) , onClick : ( ) => setRange ( '1W' ) } ,
119+ { label : t ( '1M' ) , onClick : ( ) => setRange ( '1M' ) } ,
120+ { label : t ( '3M' ) , onClick : ( ) => setRange ( '3M' ) } ,
38121 ] ,
39- [ getRates , t ] ,
122+ [ t ] ,
40123 ) ;
41124
42- const formatDate = useCallback ( ( date : Date , year ?: boolean ) => {
43- return date . toLocaleDateString ( 'en-us' , { year : year ? 'numeric' : undefined , month : 'short' , day : '2-digit' } ) ;
44- } , [ ] ) ;
125+ const formatDate = useCallback (
126+ ( date : Date , year ?: boolean ) =>
127+ date . toLocaleDateString ( 'en-us' , { year : year ? 'numeric' : undefined , month : 'short' , day : '2-digit' } ) ,
128+ [ ] ,
129+ ) ;
45130
46131 const onShowUtilizationChange = useCallback ( ( ) => {
47132 setShowUtilization ( ( prev ) => ! prev ) ;
@@ -55,7 +140,7 @@ const HistoricalRateChart: FC<Props> = ({ symbol }) => {
55140 } , [ showUtilization , symbol ] ) ;
56141
57142 return (
58- < Box display = "flex" flexDirection = "column" width = "100%" height = "100%" gap = { 2 } >
143+ < Box data-testid = "historical-rate-chart" display = "flex" flexDirection = "column" width = "100%" height = "100%" gap = { 2 } >
59144 < Box display = "flex" justifyContent = "space-between" >
60145 < Typography variant = "h6" fontSize = "16px" >
61146 { t ( 'Historical Variable Rates' ) }
@@ -67,14 +152,22 @@ const HistoricalRateChart: FC<Props> = ({ symbol }) => {
67152 < ResponsiveContainer width = "100%" height = "100%" >
68153 { loading ? (
69154 < LoadingChart />
155+ ) : points . length < 2 ? (
156+ < Box display = "flex" width = "100%" height = "100%" alignItems = "center" justifyContent = "center" >
157+ < Typography color = "grey.500" variant = "subtitle2" fontSize = "14px" >
158+ { t ( 'Not enough variable rate activity to chart yet.' ) }
159+ </ Typography >
160+ </ Box >
70161 ) : (
71- < LineChart data = { sortedRates } margin = { { top : 5 , bottom : 5 } } >
162+ < LineChart data = { points } margin = { { top : 5 , bottom : 5 } } >
72163 < CartesianGrid horizontal vertical = { false } stroke = { palette . grey [ 300 ] } />
73164 < XAxis
165+ dataKey = "date"
166+ type = "number"
167+ domain = { [ 'dataMin' , 'dataMax' ] }
74168 minTickGap = { 50 }
75169 padding = { { left : 20 , right : 30 } }
76- dataKey = "date"
77- tickFormatter = { ( value ) => ( value instanceof Date ? formatDate ( value as Date ) : '' ) }
170+ tickFormatter = { ( value ) => formatDate ( new Date ( value as number ) ) }
78171 stroke = "#B4BABF"
79172 fontSize = "12px"
80173 height = { 20 }
@@ -99,9 +192,9 @@ const HistoricalRateChart: FC<Props> = ({ symbol }) => {
99192 />
100193 ) }
101194 < Tooltip
102- labelFormatter = { ( value ) => ( value instanceof Date ? formatDate ( value as Date , true ) : '' ) }
195+ labelFormatter = { ( value ) => formatDate ( new Date ( value as number ) , true ) }
103196 formatter = { ( value ) => toPercentage ( value as number ) }
104- content = { < TooltipChart itemSorter = { ( a , b ) => ( a . value > b . value ? - 1 : 1 ) } /> }
197+ content = { < TooltipChart sortItems = { ( a , b ) => ( a . value > b . value ? - 1 : 1 ) } /> }
105198 />
106199 < Line
107200 yAxisId = "left"
@@ -141,12 +234,7 @@ const HistoricalRateChart: FC<Props> = ({ symbol }) => {
141234 < Checkbox
142235 size = "small"
143236 onChange = { onShowUtilizationChange }
144- sx = { {
145- color : palette . blue ,
146- '&.Mui-checked' : {
147- color : palette . blue ,
148- } ,
149- } }
237+ sx = { { color : palette . blue , '&.Mui-checked' : { color : palette . blue } } }
150238 />
151239 }
152240 label = {
0 commit comments