diff --git a/apps/frontend/.env.example b/apps/frontend/.env.example index 30a2abf58..24959e971 100644 --- a/apps/frontend/.env.example +++ b/apps/frontend/.env.example @@ -1,3 +1,5 @@ +REACT_APP_RATES_HISTORY_API_URL=https://bob-mm-cache.test.sovryn.app/data/rates-history + REACT_APP_GRAPH_RSK=https://subgraph.test.sovryn.app/subgraphs/name/DistributedCollective/sovryn-subgraph REACT_APP_GRAPH_BOB=https://bob-ambient-subgraph.test.sovryn.app/subgraphs/name/DistributedCollective/bob-ambient-subgraph @@ -23,4 +25,4 @@ REACT_APP_ENABLE_SERVICE_WORKER=false REACT_APP_SIMULATE_TX=false REACT_APP_ESTIMATOR_URI=https://simulator.sovryn.app -REACT_APP_DATADOG_CLIENT_TOKEN= \ No newline at end of file +REACT_APP_DATADOG_CLIENT_TOKEN= diff --git a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/AaveReserveOverviewPage.tsx b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/AaveReserveOverviewPage.tsx index 68857ca5c..eea55516d 100644 --- a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/AaveReserveOverviewPage.tsx +++ b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/AaveReserveOverviewPage.tsx @@ -9,8 +9,8 @@ import { Paragraph, Tabs, TabSize, TabType } from '@sovryn/ui'; import { useAaveReservesData } from '../../../hooks/aave/useAaveReservesData'; import { translations } from '../../../locales/i18n'; -import { TAB_ITEMS } from './AaveReserveOverviewPage.constants'; import { COMMON_SYMBOLS } from '../../../utils/asset'; +import { TAB_ITEMS } from './AaveReserveOverviewPage.constants'; import { BorrowDetailsGraph } from './components/BorrowDetailsGraph/BorrowDetailsGraph'; import { EModeDetails } from './components/EModeDetails/EModeDetails'; import { InterestRateModelGraph } from './components/InterestRateModelGraph/InterestRateModelGraph'; @@ -84,7 +84,7 @@ const AaveReserveOverviewPage: FC = () => {
diff --git a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/BorrowDetailsGraph/BorrowDetailsGraph.tsx b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/BorrowDetailsGraph/BorrowDetailsGraph.tsx index ae6ce89e1..68e9fdb7f 100644 --- a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/BorrowDetailsGraph/BorrowDetailsGraph.tsx +++ b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/BorrowDetailsGraph/BorrowDetailsGraph.tsx @@ -7,7 +7,13 @@ import { Accordion, Link, Paragraph } from '@sovryn/ui'; import { AmountRenderer } from '../../../../2_molecules/AmountRenderer/AmountRenderer'; import { StatisticsCard } from '../../../../2_molecules/StatisticsCard/StatisticsCard'; +import { AAVE_CONTRACT_ADDRESSES } from '../../../../../constants/aave'; import { Reserve } from '../../../../../hooks/aave/useAaveReservesData'; +import { + useAaveReservesHistory, + ReserveRateTimeRange, + ESupportedTimeRanges, +} from '../../../../../hooks/aave/useAaveReservesHistory'; import { useIsMobile } from '../../../../../hooks/useIsMobile'; import { translations } from '../../../../../locales/i18n'; import { formatAmountWithSuffix } from '../../../../../utils/math'; @@ -28,6 +34,23 @@ export const BorrowDetailsGraph: FC = ({ const borrowStats = useMemo(() => normalizeBorrowStats(reserve), [reserve]); + const [timeRange, setTimeRange] = useState( + ESupportedTimeRanges.OneMonth, + ); + const { data: history } = useAaveReservesHistory( + `${reserve.underlyingAsset}${AAVE_CONTRACT_ADDRESSES.POOL_ADDRESSES_PROVIDER}`, + timeRange, + ); + + const borrowChartData = useMemo( + () => + history.map(i => ({ + x: i.date, + y: i.variableBorrowRate * 100, + })), + [history], + ); + return ( = ({
diff --git a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/BorrowDetailsGraph/components/Chart/Chart.tsx b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/BorrowDetailsGraph/components/Chart/Chart.tsx index 77febba4a..9f93e5f24 100644 --- a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/BorrowDetailsGraph/components/Chart/Chart.tsx +++ b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/BorrowDetailsGraph/components/Chart/Chart.tsx @@ -3,6 +3,8 @@ import React, { FC, useCallback, useEffect, useRef } from 'react'; import ChartLibrary from 'chart.js/auto'; import 'chartjs-adapter-date-fns'; +import { TimeRangeButtons } from '../../../TimeRangeButtons/TimeRangeButtons'; +import { ReserveRateTimeRange } from './../../../../../../../hooks/aave/useAaveReservesHistory'; import { CUSTOM_CANVAS_BACKGROUND_COLOR, GRID_COLOR, @@ -13,9 +15,10 @@ import { htmlLegendPlugin } from './Chart.utils'; type ChartProps = { input: ChartData; + onTimeRangeChange: (range: ReserveRateTimeRange) => void; }; -export const Chart: FC = ({ input }) => { +export const Chart: FC = ({ input, onTimeRangeChange }) => { const canvas = useRef(null); const chartRef = useRef(null); @@ -111,7 +114,10 @@ export const Chart: FC = ({ input }) => { return (
- +
+ + +
); diff --git a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/InterestRateModelGraph.tsx b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/InterestRateModelGraph.tsx index 9bd05ed4d..98e9dd2ee 100644 --- a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/InterestRateModelGraph.tsx +++ b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/InterestRateModelGraph.tsx @@ -1,6 +1,7 @@ import React, { FC, useMemo, useState } from 'react'; import { t } from 'i18next'; +import { useSearchParams } from 'react-router-dom'; import { theme } from '@sovryn/tailwindcss-config'; import { Accordion, Link } from '@sovryn/ui'; @@ -9,12 +10,13 @@ import { Decimal } from '@sovryn/utils'; import { AmountRenderer } from '../../../../2_molecules/AmountRenderer/AmountRenderer'; import { StatisticsCard } from '../../../../2_molecules/StatisticsCard/StatisticsCard'; import { AAVE_CONTRACT_ADDRESSES } from '../../../../../constants/aave'; +import { useAaveInterestRatesData } from '../../../../../hooks/aave/useAaveRates'; import { Reserve } from '../../../../../hooks/aave/useAaveReservesData'; import { useIsMobile } from '../../../../../hooks/useIsMobile'; import { translations } from '../../../../../locales/i18n'; import { getBobExplorerUrl } from '../../../../../utils/helpers'; import { Chart } from './components/Chart/Chart'; -import { RatesData } from './components/Chart/Chart.types'; +import { COMMON_SYMBOLS } from '../../../../../utils/asset'; const pageTranslations = translations.aaveReserveOverviewPage.interestRateModel; @@ -25,6 +27,9 @@ type InterestRateModelGraphProps = { export const InterestRateModelGraph: FC = ({ reserve, }) => { + const [searchParams] = useSearchParams(); + const symbol = searchParams.get('asset') || COMMON_SYMBOLS.ETH; + const { data: rates } = useAaveInterestRatesData(symbol); const { isMobile } = useIsMobile(); const [open, setOpen] = useState(true); @@ -46,6 +51,9 @@ export const InterestRateModelGraph: FC = ({ [reserve.borrowUsageRatio], ); + if (!rates) { + return null; + } return ( = ({ {/* statistics */} diff --git a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.tsx b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.tsx index 8ef847dc6..36b05f3a5 100644 --- a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.tsx +++ b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.tsx @@ -33,7 +33,7 @@ export const Chart: FC = ({ meta, rates }) => { () => CHART_PERCENTAGES.map(x => ({ x: x * 100, - y: calculateVariableInterestRateModel(x, rates) * 100, + y: calculateVariableInterestRateModel(x, rates).mul(100).toNumber(), })), [rates], ); @@ -41,7 +41,7 @@ export const Chart: FC = ({ meta, rates }) => { const optimalPercentage = useMemo( () => rates.optimalUsageRatio - ? parseFloat((parseFloat(rates.optimalUsageRatio) * 100).toFixed(2)) + ? Math.round(rates.optimalUsageRatio.mul(100).toNumber() * 100) / 100 : 0, [rates.optimalUsageRatio], ); @@ -49,7 +49,7 @@ export const Chart: FC = ({ meta, rates }) => { const currentPercentage = useMemo( () => rates.currentUsageRatio - ? parseFloat((parseFloat(rates.currentUsageRatio) * 100).toFixed(2)) + ? Math.round(rates.currentUsageRatio.mul(100).toNumber() * 100) / 100 : 0, [rates.currentUsageRatio], ); diff --git a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.types.ts b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.types.ts index 8a45374fa..11a089eb5 100644 --- a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.types.ts +++ b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.types.ts @@ -1,3 +1,5 @@ +import { Decimal } from '@sovryn/utils'; + export type MockData = { data1: T[]; data2: T[]; @@ -8,13 +10,13 @@ export type MockData = { }; export interface RatesData { - currentUsageRatio: string; - optimalUsageRatio: string; - baseVariableBorrowRate: string; - variableRateSlope1: string; - variableRateSlope2: string; + currentUsageRatio: Decimal; + optimalUsageRatio: Decimal; + baseVariableBorrowRate: Decimal; + variableRateSlope1: Decimal; + variableRateSlope2: Decimal; underlyingAsset: string; name: string; symbol: string; - decimals: string; + decimals: number; } diff --git a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.utils.ts b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.utils.ts index 2588ef4e6..463bfc902 100644 --- a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.utils.ts +++ b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/InterestRateModelGraph/components/Chart/Chart.utils.ts @@ -1,6 +1,8 @@ +import { Decimal } from '@sovryn/utils'; + import { RatesData } from './Chart.types'; -const getOrCreateLegendList = id => { +const getOrCreateLegendList = (id: string) => { const legendContainer = document.getElementById(id); if (!legendContainer) { return; @@ -92,35 +94,41 @@ export const htmlLegendPlugin = { export const calculateInterestRateModel = ( utilization: number, - baseRate: number, - optimalUtilization: number, - initialSlope: number, - secondarySlope: number, -): number => { + baseRate: Decimal, + optimalUtilization: Decimal, + initialSlope: Decimal, + secondarySlope: Decimal, +): Decimal => { if (utilization === 0) { - return 0; + return Decimal.ZERO; } - if (utilization <= optimalUtilization) { - return baseRate + (utilization / optimalUtilization) * initialSlope; + const utilizationDecimal = Decimal.from(utilization); + + if (utilizationDecimal.lte(optimalUtilization)) { + return baseRate.add( + utilizationDecimal.div(optimalUtilization).mul(initialSlope), + ); } - return ( - baseRate + - initialSlope + - ((utilization - optimalUtilization) / (1 - optimalUtilization)) * - secondarySlope - ); + return baseRate + .add(initialSlope) + .add( + utilizationDecimal + .sub(optimalUtilization) + .div(Decimal.from(1).sub(optimalUtilization)) + .mul(secondarySlope), + ); }; export const calculateVariableInterestRateModel = ( utilization: number, rates: RatesData, -): number => { - const baseRate = parseFloat(rates.baseVariableBorrowRate); - const optimalUtilization = parseFloat(rates.optimalUsageRatio); - const initialSlope = parseFloat(rates.variableRateSlope1); - const secondarySlope = parseFloat(rates.variableRateSlope2); +) => { + const baseRate = rates.baseVariableBorrowRate; + const optimalUtilization = rates.optimalUsageRatio; + const initialSlope = rates.variableRateSlope1; + const secondarySlope = rates.variableRateSlope2; return calculateInterestRateModel( utilization, diff --git a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/SupplyDetailsGraph/SupplyDetailsGraph.tsx b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/SupplyDetailsGraph/SupplyDetailsGraph.tsx index 81e49fece..1f24b7766 100644 --- a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/SupplyDetailsGraph/SupplyDetailsGraph.tsx +++ b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/SupplyDetailsGraph/SupplyDetailsGraph.tsx @@ -8,7 +8,13 @@ import { Accordion, Icon, IconNames, Paragraph } from '@sovryn/ui'; import { AmountRenderer } from '../../../../2_molecules/AmountRenderer/AmountRenderer'; import { StatisticsCard } from '../../../../2_molecules/StatisticsCard/StatisticsCard'; +import { AAVE_CONTRACT_ADDRESSES } from '../../../../../constants/aave'; import { Reserve } from '../../../../../hooks/aave/useAaveReservesData'; +import { + ESupportedTimeRanges, + ReserveRateTimeRange, + useAaveReservesHistory, +} from '../../../../../hooks/aave/useAaveReservesHistory'; import { useIsMobile } from '../../../../../hooks/useIsMobile'; import { translations } from '../../../../../locales/i18n'; import { formatAmountWithSuffix } from '../../../../../utils/math'; @@ -29,6 +35,23 @@ export const SupplyDetailsGraph: FC = ({ const supplyStats = useMemo(() => normalizeSupplyStats(reserve), [reserve]); + const [timeRange, setTimeRange] = useState( + ESupportedTimeRanges.OneMonth, + ); + const { data: history } = useAaveReservesHistory( + `${reserve.underlyingAsset}${AAVE_CONTRACT_ADDRESSES.POOL_ADDRESSES_PROVIDER}`, + timeRange, + ); + + const supplyChartData = useMemo( + () => + history.map(i => ({ + x: i.date, + y: i.liquidityRate * 100, + })), + [history], + ); + return ( = ({
diff --git a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/SupplyDetailsGraph/components/Chart/Chart.tsx b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/SupplyDetailsGraph/components/Chart/Chart.tsx index 95525e619..10b4d6097 100644 --- a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/SupplyDetailsGraph/components/Chart/Chart.tsx +++ b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/SupplyDetailsGraph/components/Chart/Chart.tsx @@ -3,6 +3,8 @@ import React, { FC, useCallback, useEffect, useRef } from 'react'; import ChartLibrary from 'chart.js/auto'; import 'chartjs-adapter-date-fns'; +import { ReserveRateTimeRange } from '../../../../../../../hooks/aave/useAaveReservesHistory'; +import { TimeRangeButtons } from '../../../TimeRangeButtons/TimeRangeButtons'; import { CUSTOM_CANVAS_BACKGROUND_COLOR, GRID_COLOR, @@ -13,9 +15,10 @@ import { htmlLegendPlugin } from './Chart.utils'; type ChartProps = { input: InputData<{ x: number; y: number }>; + onTimeRangeChange: (range: ReserveRateTimeRange) => void; }; -export const Chart: FC = ({ input }) => { +export const Chart: FC = ({ input, onTimeRangeChange }) => { const canvas = useRef(null); const chartRef = useRef(null); @@ -113,7 +116,10 @@ export const Chart: FC = ({ input }) => { return (
- +
+ + +
); diff --git a/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/TimeRangeButtons/TimeRangeButtons.tsx b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/TimeRangeButtons/TimeRangeButtons.tsx new file mode 100644 index 000000000..7c634cce2 --- /dev/null +++ b/apps/frontend/src/app/5_pages/AaveReserveOverviewPage/components/TimeRangeButtons/TimeRangeButtons.tsx @@ -0,0 +1,60 @@ +import React, { FC, useCallback, useState } from 'react'; + +import { + ESupportedTimeRanges, + ReserveRateTimeRange, +} from '../../../../../hooks/aave/useAaveReservesHistory'; + +interface TimeRangeButtonsProps { + onChange: (range: ReserveRateTimeRange) => void; +} + +export const TimeRangeButtons: FC = ({ onChange }) => { + const [activeRange, setActiveRange] = useState( + ESupportedTimeRanges.OneMonth, + ); + + const handleClick = useCallback( + (range: ReserveRateTimeRange) => { + setActiveRange(range); + onChange(range); + }, + [onChange], + ); + + return ( +
+ {' '} + + + +
+ ); +}; diff --git a/apps/frontend/src/constants/aave.ts b/apps/frontend/src/constants/aave.ts index 9c5fa4dfb..fe83dccc3 100644 --- a/apps/frontend/src/constants/aave.ts +++ b/apps/frontend/src/constants/aave.ts @@ -1,4 +1,5 @@ import { IS_MAINNET } from '../config/chains'; + import { decimalic } from '../utils/math'; export const EMODE_DISABLED_ID = 0; @@ -25,4 +26,5 @@ export const AAVE_CONTRACT_ADDRESSES = IS_MAINNET WETH: '0x8CEc2719a2e896A11eA3f10406EfDb6Ad87281D9', TREASURY: '0x2a9d8f5b2f7b8f5b4b5d5e7f3b4b5d5b5d5d5d5d', INTEREST_RATE_STRATEGY: '0x847A3364Cc5fE389283bD821cfC8A477288D9e82', + RATES_HISTORY_API_URL: process.env.REACT_APP_RATES_HISTORY_API_URL, }; diff --git a/apps/frontend/src/hooks/aave/useAaveRates.tsx b/apps/frontend/src/hooks/aave/useAaveRates.tsx new file mode 100644 index 000000000..24bf33a58 --- /dev/null +++ b/apps/frontend/src/hooks/aave/useAaveRates.tsx @@ -0,0 +1,70 @@ +import { useMemo, useState } from 'react'; + +import { formatUnits } from 'ethers/lib/utils'; + +import { Decimal } from '@sovryn/utils'; + +import { AaveCalculations } from '../../utils/aave/AaveCalculations'; +import { RAY_DECIMALS } from '../../utils/math'; +import { useAaveReservesData } from './useAaveReservesData'; + +export interface RatesDataResult { + currentUsageRatio: Decimal; + optimalUsageRatio: Decimal; + baseVariableBorrowRate: Decimal; + variableRateSlope1: Decimal; + variableRateSlope2: Decimal; + underlyingAsset: string; + name: string; + symbol: string; + decimals: number; +} + +export const useAaveInterestRatesData = ( + symbol: string, +): { + data: RatesDataResult | null; + error: string | null; +} => { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const { reserves } = useAaveReservesData(); + useMemo(() => { + const reserve = reserves.find( + r => r.symbol.toLocaleLowerCase() === symbol.toLocaleLowerCase(), + ); + if (!reserve) { + return; + } + try { + const utilizationRate = AaveCalculations.calculateUtilizationRate( + reserve.decimals, + reserve.totalDebt, + reserve.availableLiquidity, + ); + setData({ + currentUsageRatio: utilizationRate, + optimalUsageRatio: Decimal.from( + formatUnits(reserve.optimalUsageRatio, RAY_DECIMALS), + ), + baseVariableBorrowRate: Decimal.from( + formatUnits(reserve.baseVariableBorrowRate, RAY_DECIMALS), + ), + variableRateSlope1: Decimal.from( + formatUnits(reserve.variableRateSlope1, RAY_DECIMALS), + ), + variableRateSlope2: Decimal.from( + formatUnits(reserve.variableRateSlope2, RAY_DECIMALS), + ), + underlyingAsset: reserve.underlyingAsset, + name: reserve.name, + symbol: reserve.symbol, + decimals: reserve.decimals, + }); + } catch (error) { + setError(error.message); + } + }, [reserves, symbol]); + + return { data, error }; +}; diff --git a/apps/frontend/src/hooks/aave/useAaveReservesHistory.tsx b/apps/frontend/src/hooks/aave/useAaveReservesHistory.tsx new file mode 100644 index 000000000..790dbb950 --- /dev/null +++ b/apps/frontend/src/hooks/aave/useAaveReservesHistory.tsx @@ -0,0 +1,152 @@ +import { useCallback, useEffect, useState } from 'react'; + +import axios from 'axios'; +import dayjs from 'dayjs'; + +import { AAVE_CONTRACT_ADDRESSES } from '../../constants/aave'; + +export enum ESupportedTimeRanges { + OneMonth = '1m', + ThreeMonths = '3m', + SixMonths = '6m', + OneYear = '1y', + TwoYears = '2y', + FiveYears = '5y', +} + +export const reserveRateTimeRangeOptions = [ + ESupportedTimeRanges.OneMonth, + ESupportedTimeRanges.SixMonths, + ESupportedTimeRanges.OneYear, +]; +export type ReserveRateTimeRange = typeof reserveRateTimeRangeOptions[number]; + +type RatesHistoryParams = { + from: number; + resolutionInHours: number; +}; + +type APIResponse = { + liquidityRate_avg: number; + variableBorrowRate_avg: number; + stableBorrowRate_avg: number; + utilizationRate_avg: number; + x: { year: number; month: number; date: number; hours: number }; +}; + +const requestCache = new Map>(); +const fetchStats = async ( + reserveId: string, + timeRange: ReserveRateTimeRange, + endpointURL: string, +): Promise => { + const { from, resolutionInHours } = resolutionForTimeRange(timeRange); + const qs = `reserveId=${reserveId}&from=${from}&resolutionInHours=${resolutionInHours}`; + const url = `${endpointURL}?${qs}`; + + if (requestCache.has(url)) { + return requestCache.get(url)!; + } + + const requestPromise = axios + .get(url) + .then(response => response.data) + .finally(() => { + requestCache.delete(url); + }); + + requestCache.set(url, requestPromise); + + return requestPromise; +}; + +const resolutionForTimeRange = ( + timeRange: ReserveRateTimeRange, +): RatesHistoryParams => { + switch (timeRange) { + case ESupportedTimeRanges.OneMonth: + return { + from: dayjs().subtract(30, 'day').unix(), + resolutionInHours: 6, + }; + case ESupportedTimeRanges.SixMonths: + return { + from: dayjs().subtract(6, 'month').unix(), + resolutionInHours: 24, + }; + case ESupportedTimeRanges.OneYear: + return { + from: dayjs().subtract(1, 'year').unix(), + resolutionInHours: 24, + }; + default: + return { + // Return today as a fallback + from: dayjs().unix(), + resolutionInHours: 6, + }; + } +}; + +export type FormattedReserveHistoryItem = { + date: number; + liquidityRate: number; + variableBorrowRate: number; +}; + +export function useAaveReservesHistory( + reserveId: string, + timeRange: ReserveRateTimeRange, +) { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const [data, setData] = useState([]); + + const refetchData = useCallback(() => { + if (reserveId && AAVE_CONTRACT_ADDRESSES.RATES_HISTORY_API_URL) { + // reset + setLoading(true); + setError(false); + setData([]); + fetchStats( + reserveId, + timeRange, + AAVE_CONTRACT_ADDRESSES.RATES_HISTORY_API_URL, + ) + .then((data: APIResponse[]) => { + setData( + data.map(d => ({ + date: dayjs() + .set('year', d.x.year) + .set('month', d.x.month) + .set('date', d.x.date) + .set('hour', d.x.hours) + .valueOf(), + liquidityRate: d.liquidityRate_avg, + variableBorrowRate: d.variableBorrowRate_avg, + })), + ); + }) + .catch(e => { + console.error( + 'useReservesHistory(): Failed to fetch historical reserve data.', + e, + ); + setError(true); + }) + .finally(() => setLoading(false)); + } + + return () => null; + }, [reserveId, timeRange]); + + useEffect(() => { + refetchData(); + }, [refetchData]); + return { + loading, + data, + error, + refetch: refetchData, + }; +} diff --git a/apps/frontend/src/locales/en/translations.json b/apps/frontend/src/locales/en/translations.json index d175ee305..94f1ff7cb 100644 --- a/apps/frontend/src/locales/en/translations.json +++ b/apps/frontend/src/locales/en/translations.json @@ -999,7 +999,7 @@ "of": "of", "apy": "APY", "chart": { - "label1": "Supply APR" + "suppApr": "Supply APR" } }, "borrowDetails": { @@ -1015,7 +1015,7 @@ "collectorContract": "Collector contract", "viewContract": "View contract", "chart": { - "label1": "Borrow APR, variable" + "aprVarLabel": "Borrow APR, variable" } }, "interestRateModel": { @@ -1027,7 +1027,7 @@ "collectorContract": "Collector contract", "viewContract": "View contract", "chart": { - "label1": "Borrow APR, variable" + "aprVarLabel": "Borrow APR, variable" } } }, diff --git a/apps/frontend/src/utils/aave/AaveCalculations.ts b/apps/frontend/src/utils/aave/AaveCalculations.ts index 910b964b5..00246dabd 100644 --- a/apps/frontend/src/utils/aave/AaveCalculations.ts +++ b/apps/frontend/src/utils/aave/AaveCalculations.ts @@ -1,3 +1,5 @@ +import { utils } from 'ethers'; + import { Decimal } from '@sovryn/utils'; import { UserSummary } from './AaveUserReservesSummary'; @@ -128,4 +130,41 @@ export class AaveCalculations { } return borrowSize.mul(currentLiquidationThreshold).div(collateralBalance); } + + static calculateUtilizationRate = ( + decimals: number, + totalDebt: string, + availableLiquidity: string, + ): Decimal => { + const totalBorrowBigInt = BigInt( + utils.parseUnits(totalDebt, decimals).toString(), + ); + const availableLiquidityBigInt = BigInt(availableLiquidity); + + const totalSupplyBigInt = totalBorrowBigInt + availableLiquidityBigInt; + + if (totalSupplyBigInt === BigInt(0)) { + return Decimal.from(0); + } + + const utilizationRateBigInt = + (totalBorrowBigInt * BigInt(10 ** decimals)) / totalSupplyBigInt; + + const utilizationRate = utils.formatUnits( + utilizationRateBigInt.toString(), + decimals, + ); + + const utilizationRateDecimal = Decimal.from(utilizationRate); + + const isBetweenZeroAndOne = + utilizationRateDecimal.gte(Decimal.from(0)) && + utilizationRateDecimal.lte(Decimal.from(1)); + + if (!isBetweenZeroAndOne) { + return Decimal.from(0); + } + + return utilizationRateDecimal; + }; } diff --git a/apps/frontend/src/utils/math.ts b/apps/frontend/src/utils/math.ts index 68b86bf6c..cd4b0d1a3 100644 --- a/apps/frontend/src/utils/math.ts +++ b/apps/frontend/src/utils/math.ts @@ -11,6 +11,8 @@ const DEFAULT_DECIMALS = 6; const DEFAULT_DECIMALS_SEPARATOR = '.'; const DEFAULT_THOUSANDS_SEPARATOR = ','; +export const RAY_DECIMALS = 27; + const unitNames = ['wei', 'kwei', 'mwei', 'gwei', 'szabo', 'finney', 'ether']; // helper function to convert any type of ethers value to wei.