From d4515ebc77d8c5b41315848327d4e51d2115a9fe Mon Sep 17 00:00:00 2001 From: defispartan Date: Mon, 16 Dec 2024 23:42:50 -0600 Subject: [PATCH 01/11] feat: scaffold APY range select --- src/components/HistoricalAPYRow.tsx | 100 ++++++++++++++++++ src/components/TitleWithSearchBar.tsx | 7 +- .../markets/MarketAssetsListContainer.tsx | 47 ++++++-- 3 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 src/components/HistoricalAPYRow.tsx diff --git a/src/components/HistoricalAPYRow.tsx b/src/components/HistoricalAPYRow.tsx new file mode 100644 index 0000000000..81f74032af --- /dev/null +++ b/src/components/HistoricalAPYRow.tsx @@ -0,0 +1,100 @@ +import { SxProps, Theme, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; + +const supportedHistoricalTimeRangeOptions = ['Now', '30D', '60D', '90D'] as const; + +export enum ESupportedAPYTimeRanges { + Now = 'Now', + ThirtyDays = '30D', + SixtyDays = '60D', + NinetyDays = '90D', +} + +export const reserveHistoricalRateTimeRangeOptions = [ + ESupportedAPYTimeRanges.Now, + ESupportedAPYTimeRanges.ThirtyDays, + ESupportedAPYTimeRanges.SixtyDays, + ESupportedAPYTimeRanges.NinetyDays, +]; + +export type ReserveHistoricalRateTimeRange = typeof reserveHistoricalRateTimeRangeOptions[number]; + +export interface TimeRangeSelectorProps { + disabled?: boolean; + selectedTimeRange: ESupportedAPYTimeRanges; + onTimeRangeChanged: (value: ESupportedAPYTimeRanges) => void; + sx?: { + buttonGroup: SxProps; + button: SxProps; + }; +} + +export const HistoricalAPYRow = ({ + disabled = false, + selectedTimeRange, + onTimeRangeChanged, + ...props +}: TimeRangeSelectorProps) => { + const handleChange = ( + _event: React.MouseEvent, + newInterval: ESupportedAPYTimeRanges + ) => { + if (newInterval !== null) { + onTimeRangeChanged(newInterval); + } + }; + + return ( +
+ APY + + {supportedHistoricalTimeRangeOptions.map((interval) => { + return ( + | undefined => ({ + '&.MuiToggleButtonGroup-grouped:not(.Mui-selected), &.MuiToggleButtonGroup-grouped&.Mui-disabled': + { + border: '0.5px solid transparent', + backgroundColor: 'background.surface', + color: 'action.disabled', + }, + '&.MuiToggleButtonGroup-grouped&.Mui-selected': { + borderRadius: '4px', + border: `0.5px solid ${theme.palette.divider}`, + boxShadow: '0px 2px 1px rgba(0, 0, 0, 0.05), 0px 0px 1px rgba(0, 0, 0, 0.25)', + backgroundColor: 'background.paper', + }, + ...props.sx?.button, + })} + > + {interval} + + ); + })} + +
+ ); +}; diff --git a/src/components/TitleWithSearchBar.tsx b/src/components/TitleWithSearchBar.tsx index 8529a3ec5a..eb4ba1056e 100644 --- a/src/components/TitleWithSearchBar.tsx +++ b/src/components/TitleWithSearchBar.tsx @@ -28,13 +28,10 @@ export const TitleWithSearchBar = ({ title, }: TitleWithSearchBarProps) => { const [showSearchBar, setShowSearchBar] = useState(false); - const { breakpoints } = useTheme(); const sm = useMediaQuery(breakpoints.down('sm')); - const showSearchIcon = sm && !showSearchBar; - const showMarketTitle = !sm || !showSearchBar; - + const showMarketTitle = (!sm || !showSearchBar) && !!title; const handleCancelClick = () => { setShowSearchBar(false); onSearchTermChange(''); @@ -46,7 +43,7 @@ export const TitleWithSearchBar = ({ width: '100%', display: 'flex', alignItems: 'center', - justifyContent: 'space-between', + justifyContent: showMarketTitle && title ? 'space-between' : 'center', }} > {showMarketTitle && ( diff --git a/src/modules/markets/MarketAssetsListContainer.tsx b/src/modules/markets/MarketAssetsListContainer.tsx index 2660231fdc..0024e5d86a 100644 --- a/src/modules/markets/MarketAssetsListContainer.tsx +++ b/src/modules/markets/MarketAssetsListContainer.tsx @@ -16,6 +16,11 @@ import { getGhoReserve, GHO_MINTING_MARKETS, GHO_SYMBOL } from 'src/utils/ghoUti import { GENERAL } from '../../utils/mixPanelEvents'; import { GhoBanner } from './Gho/GhoBanner'; +import { + ESupportedAPYTimeRanges, + HistoricalAPYRow, + ReserveHistoricalRateTimeRange, +} from 'src/components/HistoricalAPYRow'; function shouldDisplayGhoBanner(marketTitle: string, searchTerm: string): boolean { // GHO banner is only displayed on markets where new GHO is mintable (i.e. Ethereum) @@ -75,6 +80,9 @@ export const MarketAssetsListContainer = () => { // marketFrozen && ['Fantom', 'Ethereum AMM'].includes(currentMarketData.marketTitle); const unfrozenReserves = filteredData.filter((r) => !r.isFrozen && !r.isPaused); const [showFrozenMarketsToggle, setShowFrozenMarketsToggle] = useState(false); + const [selectedTimeRange, setSelectedTimeRange] = useState( + ESupportedAPYTimeRanges.Now + ); const handleChange = () => { setShowFrozenMarketsToggle((prevState) => !prevState); @@ -85,15 +93,38 @@ export const MarketAssetsListContainer = () => { return ( +
+ {/* Left: Title */} +
+ {currentMarketData.marketTitle} assets - - } - searchPlaceholder={sm ? 'Search asset' : 'Search asset name, symbol, or address'} - /> + +
+ + {/* Center: Search Bar */} +
+ +
+ + {/* Right: Historical APY */} +
+ +
+
} > {displayGhoBanner && ( From a7d54a33dc0853fc509726d36ea17b244b8ba785 Mon Sep 17 00:00:00 2001 From: defispartan Date: Fri, 20 Dec 2024 11:10:14 -0600 Subject: [PATCH 02/11] feat: subgraph historical APY hook --- src/hooks/useHistoricalAPYData.ts | 162 ++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/hooks/useHistoricalAPYData.ts diff --git a/src/hooks/useHistoricalAPYData.ts b/src/hooks/useHistoricalAPYData.ts new file mode 100644 index 0000000000..96b4161523 --- /dev/null +++ b/src/hooks/useHistoricalAPYData.ts @@ -0,0 +1,162 @@ +import { useEffect, useState } from 'react'; +import { INDEX_CURRENT } from 'src/modules/markets/index-current-query'; +import { INDEX_HISTORY } from 'src/modules/markets/index-history-query'; + +export interface HistoricalAPYData { + underlyingAsset: string; + liquidityIndex: string; + variableBorrowIndex: string; + timestamp: string; + liquidityRate: string; + variableBorrowRate: string; +} + +interface Rates { + supplyAPY: string; + variableBorrowAPY: string; +} + +function calculateImpliedAPY( + currentLiquidityIndex: number, + previousLiquidityIndex: number, + daysBetweenIndexes: number, +): string { + if (previousLiquidityIndex <= 0 || currentLiquidityIndex <= 0) { + throw new Error("Liquidity indexes must be positive values."); + } + + const growthFactor = currentLiquidityIndex / previousLiquidityIndex; + + const annualizedGrowthFactor = Math.pow(growthFactor, 365 / daysBetweenIndexes); + + const impliedAPY = (annualizedGrowthFactor - 1); + + return impliedAPY.toString(); +} + +export const useHistoricalAPYData = ( + subgraphUrl: string, + selectedTimeRange: string +) => { + const [historicalAPYData, setHistoricalAPYData] = useState>({}); + + useEffect(() => { + const fetchHistoricalAPYData = async () => { + if (selectedTimeRange === 'Now') { + setHistoricalAPYData({}); + return; + } + + const timeRangeSecondsMap: Record = { + '30D': 30 * 24 * 60 * 60, + '60D': 60 * 24 * 60 * 60, + '90D': 90 * 24 * 60 * 60, + }; + + const timeRangeDaysMap: Record = { + '30D': 30, + '60D': 60, + '90D': 90, + }; + + const timeRangeInSeconds = timeRangeSecondsMap[selectedTimeRange]; + + if (timeRangeInSeconds === undefined) { + console.error(`Invalid time range: ${selectedTimeRange}`); + setHistoricalAPYData({}); + return; + } + + const timestamp = Math.floor(Date.now() / 1000) - timeRangeInSeconds; + + try { + const requestBody = { + query: INDEX_HISTORY, + variables: { timestamp }, + }; + const response = await fetch(subgraphUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + const requestBodyCurrent = { + query: INDEX_CURRENT, + }; + const responseCurrent = await fetch(subgraphUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBodyCurrent), + }); + + if (!response.ok || !responseCurrent.ok) { + throw new Error(`Network error: ${response.status} - ${response.statusText}`); + } + + const data = await response.json(); + const dataCurrent = await responseCurrent.json(); + + const historyByAsset: Record = {}; + const currentByAsset: Record = {}; + + data.data.reserveParamsHistoryItems.forEach((entry: any) => { + const assetKey = entry.reserve.underlyingAsset.toLowerCase(); + if (!historyByAsset[assetKey]) { + historyByAsset[assetKey] = { + underlyingAsset: assetKey, + liquidityIndex: entry.liquidityIndex, + variableBorrowIndex: entry.variableBorrowIndex, + liquidityRate: entry.liquidityRate, + variableBorrowRate: entry.variableBorrowRate, + timestamp: entry.timestamp, + }; + } + }); + + dataCurrent.data.reserveParamsHistoryItems.forEach((entry: any) => { + const assetKey = entry.reserve.underlyingAsset.toLowerCase(); + if (!currentByAsset[assetKey]) { + currentByAsset[assetKey] = { + underlyingAsset: assetKey, + liquidityIndex: entry.liquidityIndex, + variableBorrowIndex: entry.variableBorrowIndex, + liquidityRate: entry.liquidityRate, + variableBorrowRate: entry.variableBorrowRate, + timestamp: entry.timestamp, + }; + } + }); + + const allAssets = new Set([ + ...Object.keys(historyByAsset), + ...Object.keys(currentByAsset), + ]); + + const results: Record = {}; + allAssets.forEach((asset) => { + const historical = historyByAsset[asset]; + const current = currentByAsset[asset]; + + if (historical && current) { + results[asset] = { + supplyAPY: calculateImpliedAPY(Number(current.liquidityIndex), Number(historical.liquidityIndex), timeRangeDaysMap[selectedTimeRange] || 0), + variableBorrowAPY: calculateImpliedAPY(Number(current.variableBorrowIndex), Number(historical.variableBorrowIndex), timeRangeDaysMap[selectedTimeRange] || 0), + }; + } + }); + setHistoricalAPYData(results); + } catch (error) { + console.error('Error fetching historical APY data:', error); + setHistoricalAPYData({}); + } + }; + + fetchHistoricalAPYData(); + }, [selectedTimeRange]); + + return historicalAPYData; +}; From 9ce5785b2289091c7acd8719a27cf233bf8e584a Mon Sep 17 00:00:00 2001 From: defispartan Date: Fri, 20 Dec 2024 11:10:56 -0600 Subject: [PATCH 03/11] feat: index queries --- src/modules/markets/index-current-query.ts | 14 ++++++++++++++ src/modules/markets/index-history-query.ts | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/modules/markets/index-current-query.ts create mode 100644 src/modules/markets/index-history-query.ts diff --git a/src/modules/markets/index-current-query.ts b/src/modules/markets/index-current-query.ts new file mode 100644 index 0000000000..c96e1d00fa --- /dev/null +++ b/src/modules/markets/index-current-query.ts @@ -0,0 +1,14 @@ +export const INDEX_CURRENT = ` +query IndexCurrent { + reserveParamsHistoryItems(orderBy: timestamp, orderDirection: desc){ + liquidityIndex + liquidityRate + variableBorrowRate + variableBorrowIndex + timestamp + reserve{ + underlyingAsset + } + } +} +`; diff --git a/src/modules/markets/index-history-query.ts b/src/modules/markets/index-history-query.ts new file mode 100644 index 0000000000..250a958705 --- /dev/null +++ b/src/modules/markets/index-history-query.ts @@ -0,0 +1,14 @@ +export const INDEX_HISTORY = ` +query IndexHistory($timestamp: Int!) { + reserveParamsHistoryItems(where:{timestamp_lt: $timestamp}, orderBy: timestamp, orderDirection: desc){ + liquidityIndex + liquidityRate + variableBorrowRate + variableBorrowIndex + timestamp + reserve{ + underlyingAsset + } + } +} +`; From a8ad71819ff25fb11fe6aee1c273315956f71734 Mon Sep 17 00:00:00 2001 From: defispartan Date: Fri, 20 Dec 2024 11:11:42 -0600 Subject: [PATCH 04/11] feat: apply historical rates --- .../markets/MarketAssetsListContainer.tsx | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/modules/markets/MarketAssetsListContainer.tsx b/src/modules/markets/MarketAssetsListContainer.tsx index 0024e5d86a..9065fcdd51 100644 --- a/src/modules/markets/MarketAssetsListContainer.tsx +++ b/src/modules/markets/MarketAssetsListContainer.tsx @@ -10,6 +10,8 @@ import { TitleWithSearchBar } from 'src/components/TitleWithSearchBar'; import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider'; import { useProtocolDataContext } from 'src/hooks/useProtocolDataContext'; import MarketAssetsList from 'src/modules/markets/MarketAssetsList'; +import { useHistoricalAPYData } from 'src/hooks/useHistoricalAPYData'; + import { useRootStore } from 'src/store/root'; import { fetchIconSymbolAndName } from 'src/ui-config/reservePatches'; import { getGhoReserve, GHO_MINTING_MARKETS, GHO_SYMBOL } from 'src/utils/ghoUtilities'; @@ -50,12 +52,21 @@ export const MarketAssetsListContainer = () => { const ghoReserve = getGhoReserve(reserves); const displayGhoBanner = shouldDisplayGhoBanner(currentMarket, searchTerm); + const [selectedTimeRange, setSelectedTimeRange] = useState( + ESupportedAPYTimeRanges.Now + ); + + const historicalAPYData = useHistoricalAPYData( + currentMarketData.subgraphUrl ?? '', + selectedTimeRange + ); + const filteredData = reserves // Filter out any non-active reserves .filter((res) => res.isActive) // Filter out GHO if the banner is being displayed .filter((res) => (displayGhoBanner ? res !== ghoReserve : true)) - // filter out any that don't meet search term criteria + // Filter out any reserves that don't meet the search term criteria .filter((res) => { if (!searchTerm) return true; const term = searchTerm.toLowerCase().trim(); @@ -66,23 +77,37 @@ export const MarketAssetsListContainer = () => { ); }) // Transform the object for list to consume it - .map((reserve) => ({ - ...reserve, - ...(reserve.isWrappedBaseAsset - ? fetchIconSymbolAndName({ - symbol: currentNetworkConfig.baseAssetSymbol, - underlyingAsset: API_ETH_MOCK_ADDRESS.toLowerCase(), - }) - : {}), - })); + .map((reserve) => { + const historicalData = historicalAPYData[reserve.underlyingAsset.toLowerCase()]; + + return { + ...reserve, + ...(reserve.isWrappedBaseAsset + ? fetchIconSymbolAndName({ + symbol: currentNetworkConfig.baseAssetSymbol, + underlyingAsset: API_ETH_MOCK_ADDRESS.toLowerCase(), + }) + : {}), + supplyAPY: + selectedTimeRange === ESupportedAPYTimeRanges.Now + ? reserve.supplyAPY + : !!historicalData + ? historicalData.supplyAPY + : 'N/A', + variableBorrowAPY: + selectedTimeRange === ESupportedAPYTimeRanges.Now + ? reserve.variableBorrowAPY + : !!historicalData + ? historicalData.variableBorrowAPY + : 'N/A', + }; + }); + // const marketFrozen = !reserves.some((reserve) => !reserve.isFrozen); // const showFrozenMarketWarning = // marketFrozen && ['Fantom', 'Ethereum AMM'].includes(currentMarketData.marketTitle); const unfrozenReserves = filteredData.filter((r) => !r.isFrozen && !r.isPaused); const [showFrozenMarketsToggle, setShowFrozenMarketsToggle] = useState(false); - const [selectedTimeRange, setSelectedTimeRange] = useState( - ESupportedAPYTimeRanges.Now - ); const handleChange = () => { setShowFrozenMarketsToggle((prevState) => !prevState); From 7a4423988d126963741801650e620d859db6acba Mon Sep 17 00:00:00 2001 From: defispartan Date: Tue, 14 Jan 2025 21:41:25 -0600 Subject: [PATCH 05/11] feat: dynamic query for all assets --- src/modules/markets/index-current-query.ts | 43 +++++++++++++++------- src/modules/markets/index-history-query.ts | 43 +++++++++++++++------- 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/src/modules/markets/index-current-query.ts b/src/modules/markets/index-current-query.ts index c96e1d00fa..cac1d2f891 100644 --- a/src/modules/markets/index-current-query.ts +++ b/src/modules/markets/index-current-query.ts @@ -1,14 +1,31 @@ -export const INDEX_CURRENT = ` -query IndexCurrent { - reserveParamsHistoryItems(orderBy: timestamp, orderDirection: desc){ - liquidityIndex - liquidityRate - variableBorrowRate - variableBorrowIndex - timestamp - reserve{ - underlyingAsset +import { generateAliases } from "src/utils/generateSubgraphQueryAlias"; + +export const constructIndexCurrentQuery = (underlyingAssets: string[]): string => { + const aliases = generateAliases(underlyingAssets.length); + + const queries = underlyingAssets.map( + (asset, index) => ` + ${aliases[index]}: reserveParamsHistoryItems( + orderBy: timestamp, + orderDirection: desc, + first: 1, + where: { reserve_: { underlyingAsset: "${asset.toLowerCase()}" } } + ) { + liquidityIndex + liquidityRate + variableBorrowRate + variableBorrowIndex + timestamp + reserve { + underlyingAsset + } + } + ` + ); + + return ` + query IndexCurrent { + ${queries.join("\n")} } - } -} -`; + `; +}; diff --git a/src/modules/markets/index-history-query.ts b/src/modules/markets/index-history-query.ts index 250a958705..469bae5535 100644 --- a/src/modules/markets/index-history-query.ts +++ b/src/modules/markets/index-history-query.ts @@ -1,14 +1,31 @@ -export const INDEX_HISTORY = ` -query IndexHistory($timestamp: Int!) { - reserveParamsHistoryItems(where:{timestamp_lt: $timestamp}, orderBy: timestamp, orderDirection: desc){ - liquidityIndex - liquidityRate - variableBorrowRate - variableBorrowIndex - timestamp - reserve{ - underlyingAsset +import { generateAliases } from "src/utils/generateSubgraphQueryAlias"; + +export const constructIndexHistoryQuery = (underlyingAssets: string[]): string => { + const aliases = generateAliases(underlyingAssets.length); + + const queries = underlyingAssets.map( + (asset, index) => ` + ${aliases[index]}: reserveParamsHistoryItems( + where: { timestamp_lt: $timestamp, reserve_: { underlyingAsset: "${asset.toLowerCase()}" } }, + orderBy: timestamp, + orderDirection: desc, + first: 1 + ) { + liquidityIndex + liquidityRate + variableBorrowRate + variableBorrowIndex + timestamp + reserve { + underlyingAsset + } + } + ` + ); + + return ` + query IndexHistory($timestamp: Int!) { + ${queries.join("\n")} } - } -} -`; + `; +}; From a3f62826a6549861555fce8603d299932f0beb86 Mon Sep 17 00:00:00 2001 From: defispartan Date: Tue, 14 Jan 2025 21:41:49 -0600 Subject: [PATCH 06/11] feat: refactor hook to query all underlyingAssets --- src/hooks/useHistoricalAPYData.ts | 83 ++++++++++++++++++------------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/src/hooks/useHistoricalAPYData.ts b/src/hooks/useHistoricalAPYData.ts index 96b4161523..d6df86ca76 100644 --- a/src/hooks/useHistoricalAPYData.ts +++ b/src/hooks/useHistoricalAPYData.ts @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; -import { INDEX_CURRENT } from 'src/modules/markets/index-current-query'; -import { INDEX_HISTORY } from 'src/modules/markets/index-history-query'; +import { constructIndexCurrentQuery } from 'src/modules/markets/index-current-query'; +import { constructIndexHistoryQuery } from 'src/modules/markets/index-history-query'; +import { generateAliases } from 'src/utils/generateSubgraphQueryAlias'; export interface HistoricalAPYData { underlyingAsset: string; @@ -26,9 +27,7 @@ function calculateImpliedAPY( } const growthFactor = currentLiquidityIndex / previousLiquidityIndex; - const annualizedGrowthFactor = Math.pow(growthFactor, 365 / daysBetweenIndexes); - const impliedAPY = (annualizedGrowthFactor - 1); return impliedAPY.toString(); @@ -36,13 +35,14 @@ function calculateImpliedAPY( export const useHistoricalAPYData = ( subgraphUrl: string, - selectedTimeRange: string + selectedTimeRange: string, + underlyingAssets: string[], ) => { const [historicalAPYData, setHistoricalAPYData] = useState>({}); useEffect(() => { const fetchHistoricalAPYData = async () => { - if (selectedTimeRange === 'Now') { + if (selectedTimeRange === 'Now' || underlyingAssets.length === 0) { setHistoricalAPYData({}); return; } @@ -50,13 +50,15 @@ export const useHistoricalAPYData = ( const timeRangeSecondsMap: Record = { '30D': 30 * 24 * 60 * 60, '60D': 60 * 24 * 60 * 60, - '90D': 90 * 24 * 60 * 60, + '180D': 180 * 24 * 60 * 60, + '1Y': 365 * 24 * 60 * 60, }; const timeRangeDaysMap: Record = { '30D': 30, '60D': 60, - '90D': 90, + '180D': 180, + '1Y': 365, }; const timeRangeInSeconds = timeRangeSecondsMap[selectedTimeRange]; @@ -70,20 +72,20 @@ export const useHistoricalAPYData = ( const timestamp = Math.floor(Date.now() / 1000) - timeRangeInSeconds; try { - const requestBody = { - query: INDEX_HISTORY, + const requestBodyHistory = { + query: constructIndexHistoryQuery(underlyingAssets), variables: { timestamp }, }; - const response = await fetch(subgraphUrl, { + const responseHistory = await fetch(subgraphUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(requestBody), + body: JSON.stringify(requestBodyHistory), }); const requestBodyCurrent = { - query: INDEX_CURRENT, + query: constructIndexCurrentQuery(underlyingAssets), }; const responseCurrent = await fetch(subgraphUrl, { method: 'POST', @@ -93,19 +95,25 @@ export const useHistoricalAPYData = ( body: JSON.stringify(requestBodyCurrent), }); - if (!response.ok || !responseCurrent.ok) { - throw new Error(`Network error: ${response.status} - ${response.statusText}`); + if (!responseHistory.ok || !responseCurrent.ok) { + throw new Error(`Network error: ${responseHistory.status} - ${responseHistory.statusText}`); } - const data = await response.json(); + const dataHistory = await responseHistory.json(); const dataCurrent = await responseCurrent.json(); const historyByAsset: Record = {}; const currentByAsset: Record = {}; - data.data.reserveParamsHistoryItems.forEach((entry: any) => { - const assetKey = entry.reserve.underlyingAsset.toLowerCase(); - if (!historyByAsset[assetKey]) { + const aliases = generateAliases(underlyingAssets.length); + + underlyingAssets.forEach((_, index) => { + const alias = aliases[index]; + + const historicalEntry = dataHistory.data[alias]; + if (historicalEntry && historicalEntry.length > 0) { + const entry = historicalEntry[0]; + const assetKey = entry.reserve.underlyingAsset.toLowerCase(); historyByAsset[assetKey] = { underlyingAsset: assetKey, liquidityIndex: entry.liquidityIndex, @@ -115,11 +123,11 @@ export const useHistoricalAPYData = ( timestamp: entry.timestamp, }; } - }); - dataCurrent.data.reserveParamsHistoryItems.forEach((entry: any) => { - const assetKey = entry.reserve.underlyingAsset.toLowerCase(); - if (!currentByAsset[assetKey]) { + const currentEntry = dataCurrent.data[alias]; + if (currentEntry && currentEntry.length > 0) { + const entry = currentEntry[0]; + const assetKey = entry.reserve.underlyingAsset.toLowerCase(); currentByAsset[assetKey] = { underlyingAsset: assetKey, liquidityIndex: entry.liquidityIndex, @@ -131,23 +139,28 @@ export const useHistoricalAPYData = ( } }); - const allAssets = new Set([ - ...Object.keys(historyByAsset), - ...Object.keys(currentByAsset), - ]); - const results: Record = {}; - allAssets.forEach((asset) => { - const historical = historyByAsset[asset]; - const current = currentByAsset[asset]; + underlyingAssets.forEach((asset) => { + const assetKey = asset.toLowerCase(); + const historical = historyByAsset[assetKey]; + const current = currentByAsset[assetKey]; if (historical && current) { - results[asset] = { - supplyAPY: calculateImpliedAPY(Number(current.liquidityIndex), Number(historical.liquidityIndex), timeRangeDaysMap[selectedTimeRange] || 0), - variableBorrowAPY: calculateImpliedAPY(Number(current.variableBorrowIndex), Number(historical.variableBorrowIndex), timeRangeDaysMap[selectedTimeRange] || 0), + results[assetKey] = { + supplyAPY: calculateImpliedAPY( + Number(current.liquidityIndex), + Number(historical.liquidityIndex), + timeRangeDaysMap[selectedTimeRange] || 0 + ), + variableBorrowAPY: calculateImpliedAPY( + Number(current.variableBorrowIndex), + Number(historical.variableBorrowIndex), + timeRangeDaysMap[selectedTimeRange] || 0 + ), }; } }); + setHistoricalAPYData(results); } catch (error) { console.error('Error fetching historical APY data:', error); @@ -156,7 +169,7 @@ export const useHistoricalAPYData = ( }; fetchHistoricalAPYData(); - }, [selectedTimeRange]); + }, [selectedTimeRange, underlyingAssets]); return historicalAPYData; }; From 458bee0c18ec76357da392392d98eac7e77e6ea2 Mon Sep 17 00:00:00 2001 From: defispartan Date: Tue, 14 Jan 2025 21:42:21 -0600 Subject: [PATCH 07/11] feat: update timeframes and hook usage --- src/components/HistoricalAPYRow.tsx | 10 +++++----- src/modules/markets/MarketAssetsListContainer.tsx | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/HistoricalAPYRow.tsx b/src/components/HistoricalAPYRow.tsx index 81f74032af..7e9decb984 100644 --- a/src/components/HistoricalAPYRow.tsx +++ b/src/components/HistoricalAPYRow.tsx @@ -1,19 +1,19 @@ import { SxProps, Theme, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; -const supportedHistoricalTimeRangeOptions = ['Now', '30D', '60D', '90D'] as const; +const supportedHistoricalTimeRangeOptions = ['Now', '30D', '180D', '1Y'] as const; export enum ESupportedAPYTimeRanges { Now = 'Now', ThirtyDays = '30D', - SixtyDays = '60D', - NinetyDays = '90D', + OneHunredEightyDays = '180D', + OneYear = '1Y', } export const reserveHistoricalRateTimeRangeOptions = [ ESupportedAPYTimeRanges.Now, ESupportedAPYTimeRanges.ThirtyDays, - ESupportedAPYTimeRanges.SixtyDays, - ESupportedAPYTimeRanges.NinetyDays, + ESupportedAPYTimeRanges.OneHunredEightyDays, + ESupportedAPYTimeRanges.OneYear, ]; export type ReserveHistoricalRateTimeRange = typeof reserveHistoricalRateTimeRangeOptions[number]; diff --git a/src/modules/markets/MarketAssetsListContainer.tsx b/src/modules/markets/MarketAssetsListContainer.tsx index 268699a7e5..1933b5cae5 100644 --- a/src/modules/markets/MarketAssetsListContainer.tsx +++ b/src/modules/markets/MarketAssetsListContainer.tsx @@ -62,9 +62,11 @@ export const MarketAssetsListContainer = () => { ESupportedAPYTimeRanges.Now ); + const underlyingAssets = reserves.map((a) => a.underlyingAsset); const historicalAPYData = useHistoricalAPYData( currentMarketData.subgraphUrl ?? '', - selectedTimeRange + selectedTimeRange, + underlyingAssets ); const filteredData = reserves From 2d0c88baf1199cdae49be0079a005f89b66b4936 Mon Sep 17 00:00:00 2001 From: defispartan Date: Tue, 14 Jan 2025 21:42:43 -0600 Subject: [PATCH 08/11] feat: subgraph query alias helper --- src/utils/generateSubgraphQueryAlias.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/utils/generateSubgraphQueryAlias.ts diff --git a/src/utils/generateSubgraphQueryAlias.ts b/src/utils/generateSubgraphQueryAlias.ts new file mode 100644 index 0000000000..846c825a2c --- /dev/null +++ b/src/utils/generateSubgraphQueryAlias.ts @@ -0,0 +1,19 @@ +// subgraph requires aliasing multiple queries of the same entity with alphanumeric characters only +export const generateAliases = (length: number): string[] => { + const aliases: string[] = []; + const alphabet = 'abcdefghijklmnopqrstuvwxyz'; + + for (let i = 0; i < length; i++) { + let alias = ''; + let num = i; + + while (num >= 0) { + alias = alphabet[num % 26] + alias; + num = Math.floor(num / 26) - 1; + } + + aliases.push(alias); + } + + return aliases; +}; From 75d3b0f5414b37bbc8443e297eadf10252d51988 Mon Sep 17 00:00:00 2001 From: defispartan Date: Tue, 14 Jan 2025 22:57:11 -0600 Subject: [PATCH 09/11] feat: hide incentives for historic time range --- src/components/incentives/IncentivesCard.tsx | 46 ++++++++++--------- .../markets/MarketAssetsListContainer.tsx | 15 ++++-- src/modules/markets/MarketAssetsListItem.tsx | 24 +++++++--- .../markets/MarketAssetsListMobileItem.tsx | 24 +++++++--- src/store/protocolDataSlice.ts | 7 +++ 5 files changed, 78 insertions(+), 38 deletions(-) diff --git a/src/components/incentives/IncentivesCard.tsx b/src/components/incentives/IncentivesCard.tsx index d6bda0c6af..60b5a6c791 100644 --- a/src/components/incentives/IncentivesCard.tsx +++ b/src/components/incentives/IncentivesCard.tsx @@ -23,6 +23,7 @@ interface IncentivesCardProps { tooltip?: ReactNode; market: string; protocolAction?: ProtocolAction; + showIncentives?: boolean; } export const IncentivesCard = ({ @@ -37,6 +38,7 @@ export const IncentivesCard = ({ tooltip, market, protocolAction, + showIncentives = true, }: IncentivesCardProps) => { const isTableChangedToCards = useMediaQuery('(max-width:1125px)'); return ( @@ -65,27 +67,29 @@ export const IncentivesCard = ({ ) : ( )} - - - - - + {Boolean(showIncentives) && ( + + + + + + )} ); }; diff --git a/src/modules/markets/MarketAssetsListContainer.tsx b/src/modules/markets/MarketAssetsListContainer.tsx index 1933b5cae5..2278a5b587 100644 --- a/src/modules/markets/MarketAssetsListContainer.tsx +++ b/src/modules/markets/MarketAssetsListContainer.tsx @@ -43,12 +43,21 @@ function shouldDisplayGhoBanner(marketTitle: string, searchTerm: string): boolea export const MarketAssetsListContainer = () => { const { reserves, loading } = useAppDataContext(); - const [trackEvent, currentMarket, currentMarketData, currentNetworkConfig] = useRootStore( + const [ + trackEvent, + currentMarket, + currentMarketData, + currentNetworkConfig, + selectedTimeRange, + setSelectedTimeRange, + ] = useRootStore( useShallow((store) => [ store.trackEvent, store.currentMarket, store.currentMarketData, store.currentNetworkConfig, + store.selectedTimeRange, + store.setSelectedTimeRange, ]) ); const [searchTerm, setSearchTerm] = useState(''); @@ -58,10 +67,6 @@ export const MarketAssetsListContainer = () => { const ghoReserve = getGhoReserve(reserves); const displayGhoBanner = shouldDisplayGhoBanner(currentMarket, searchTerm); - const [selectedTimeRange, setSelectedTimeRange] = useState( - ESupportedAPYTimeRanges.Now - ); - const underlyingAssets = reserves.map((a) => a.underlyingAsset); const historicalAPYData = useHistoricalAPYData( currentMarketData.subgraphUrl ?? '', diff --git a/src/modules/markets/MarketAssetsListItem.tsx b/src/modules/markets/MarketAssetsListItem.tsx index be2af610ad..509a6de573 100644 --- a/src/modules/markets/MarketAssetsListItem.tsx +++ b/src/modules/markets/MarketAssetsListItem.tsx @@ -23,11 +23,12 @@ import { FormattedNumber } from '../../components/primitives/FormattedNumber'; import { Link, ROUTES } from '../../components/primitives/Link'; import { TokenIcon } from '../../components/primitives/TokenIcon'; import { ComputedReserveData } from '../../hooks/app-data-provider/useAppDataProvider'; +import { ESupportedAPYTimeRanges } from 'src/components/HistoricalAPYRow'; export const MarketAssetsListItem = ({ ...reserve }: ComputedReserveData) => { const router = useRouter(); - const [trackEvent, currentMarket] = useRootStore( - useShallow((store) => [store.trackEvent, store.currentMarket]) + const [trackEvent, currentMarket, selectedTimeRange] = useRootStore( + useShallow((store) => [store.trackEvent, store.currentMarket, store.selectedTimeRange]) ); const offboardingDiscussion = AssetsBeingOffboarded[currentMarket]?.[reserve.symbol]; @@ -41,6 +42,7 @@ export const MarketAssetsListItem = ({ ...reserve }: ComputedReserveData) => { currentMarket, ProtocolAction.borrow ); + const showIncentives = selectedTimeRange === ESupportedAPYTimeRanges.Now; return ( { symbolsVariant="secondary16" tooltip={ <> - {externalIncentivesTooltipsSupplySide.superFestRewards && } - {externalIncentivesTooltipsSupplySide.spkAirdrop && } + {externalIncentivesTooltipsSupplySide.superFestRewards && showIncentives && ( + + )} + {externalIncentivesTooltipsSupplySide.spkAirdrop && showIncentives && ( + + )} } market={currentMarket} protocolAction={ProtocolAction.supply} + showIncentives={showIncentives} /> @@ -131,12 +138,17 @@ export const MarketAssetsListItem = ({ ...reserve }: ComputedReserveData) => { symbolsVariant="secondary16" tooltip={ <> - {externalIncentivesTooltipsBorrowSide.superFestRewards && } - {externalIncentivesTooltipsBorrowSide.spkAirdrop && } + {externalIncentivesTooltipsBorrowSide.superFestRewards && showIncentives && ( + + )} + {externalIncentivesTooltipsBorrowSide.spkAirdrop && showIncentives && ( + + )} } market={currentMarket} protocolAction={ProtocolAction.borrow} + showIncentives={showIncentives} /> {!reserve.borrowingEnabled && Number(reserve.totalVariableDebt) > 0 && diff --git a/src/modules/markets/MarketAssetsListMobileItem.tsx b/src/modules/markets/MarketAssetsListMobileItem.tsx index fb888148bf..d6f9e036e1 100644 --- a/src/modules/markets/MarketAssetsListMobileItem.tsx +++ b/src/modules/markets/MarketAssetsListMobileItem.tsx @@ -17,10 +17,11 @@ import { Link, ROUTES } from '../../components/primitives/Link'; import { Row } from '../../components/primitives/Row'; import { ComputedReserveData } from '../../hooks/app-data-provider/useAppDataProvider'; import { ListMobileItemWrapper } from '../dashboard/lists/ListMobileItemWrapper'; +import { ESupportedAPYTimeRanges } from 'src/components/HistoricalAPYRow'; export const MarketAssetsListMobileItem = ({ ...reserve }: ComputedReserveData) => { - const [trackEvent, currentMarket] = useRootStore( - useShallow((store) => [store.trackEvent, store.currentMarket]) + const [trackEvent, currentMarket, selectedTimeRange] = useRootStore( + useShallow((store) => [store.trackEvent, store.currentMarket, store.selectedTimeRange]) ); const externalIncentivesTooltipsSupplySide = showExternalIncentivesTooltip( @@ -33,6 +34,7 @@ export const MarketAssetsListMobileItem = ({ ...reserve }: ComputedReserveData) currentMarket, ProtocolAction.borrow ); + const showIncentives = selectedTimeRange === ESupportedAPYTimeRanges.Now; return ( - {externalIncentivesTooltipsSupplySide.superFestRewards && } - {externalIncentivesTooltipsSupplySide.spkAirdrop && } + {externalIncentivesTooltipsSupplySide.superFestRewards && showIncentives && ( + + )} + {externalIncentivesTooltipsSupplySide.spkAirdrop && showIncentives && ( + + )} } market={currentMarket} protocolAction={ProtocolAction.supply} + showIncentives={showIncentives} /> @@ -124,12 +131,17 @@ export const MarketAssetsListMobileItem = ({ ...reserve }: ComputedReserveData) variant="secondary14" tooltip={ <> - {externalIncentivesTooltipsBorrowSide.superFestRewards && } - {externalIncentivesTooltipsBorrowSide.spkAirdrop && } + {externalIncentivesTooltipsBorrowSide.superFestRewards && showIncentives && ( + + )} + {externalIncentivesTooltipsBorrowSide.spkAirdrop && showIncentives && ( + + )} } market={currentMarket} protocolAction={ProtocolAction.borrow} + showIncentives={selectedTimeRange === ESupportedAPYTimeRanges.Now} /> {!reserve.borrowingEnabled && Number(reserve.totalVariableDebt) > 0 && diff --git a/src/store/protocolDataSlice.ts b/src/store/protocolDataSlice.ts index 3247c6b503..26d85d52c0 100644 --- a/src/store/protocolDataSlice.ts +++ b/src/store/protocolDataSlice.ts @@ -12,6 +12,7 @@ import { CustomMarket, MarketDataType } from '../ui-config/marketsConfig'; import { NetworkConfig } from '../ui-config/networksConfig'; import { RootStore } from './root'; import { setQueryParameter } from './utils/queryParams'; +import { ESupportedAPYTimeRanges } from 'src/components/HistoricalAPYRow'; type TypePermitParams = { reserveAddress: string; @@ -25,6 +26,8 @@ export interface ProtocolDataSlice { currentNetworkConfig: NetworkConfig; jsonRpcProvider: (chainId?: number) => providers.Provider; setCurrentMarket: (market: CustomMarket, omitQueryParameterUpdate?: boolean) => void; + selectedTimeRange: ESupportedAPYTimeRanges; + setSelectedTimeRange: (timeRange: ESupportedAPYTimeRanges) => void; tryPermit: ({ reserveAddress, isWrappedBaseAsset }: TypePermitParams) => boolean; } @@ -41,7 +44,11 @@ export const createProtocolDataSlice: StateCreator< currentMarketData: marketsData[initialMarket], currentChainId: initialMarketData.chainId, currentNetworkConfig: getNetworkConfig(initialMarketData.chainId), + selectedTimeRange: ESupportedAPYTimeRanges.Now, jsonRpcProvider: (chainId) => getProvider(chainId ?? get().currentChainId), + setSelectedTimeRange: (timeRange: ESupportedAPYTimeRanges) => { + set({ selectedTimeRange: timeRange }) + }, setCurrentMarket: (market, omitQueryParameterUpdate) => { if (!availableMarkets.includes(market as CustomMarket)) return; const nextMarketData = marketsData[market]; From 89af7c231c1172cb19d4cb2cc3fa020b3de8b507 Mon Sep 17 00:00:00 2001 From: defispartan Date: Tue, 14 Jan 2025 23:17:30 -0600 Subject: [PATCH 10/11] feat: react-query cache and loading --- src/components/incentives/IncentivesCard.tsx | 2 +- src/hooks/useHistoricalAPYData.ts | 263 +++++++++--------- .../markets/MarketAssetsListContainer.tsx | 22 +- 3 files changed, 143 insertions(+), 144 deletions(-) diff --git a/src/components/incentives/IncentivesCard.tsx b/src/components/incentives/IncentivesCard.tsx index 60b5a6c791..de9ee7532f 100644 --- a/src/components/incentives/IncentivesCard.tsx +++ b/src/components/incentives/IncentivesCard.tsx @@ -51,7 +51,7 @@ export const IncentivesCard = ({ textAlign: 'center', }} > - {value.toString() !== '-1' ? ( + {value.toString() !== '-1' && value.toString() !== 'N/A' ? ( { - const [historicalAPYData, setHistoricalAPYData] = useState>({}); - - useEffect(() => { - const fetchHistoricalAPYData = async () => { - if (selectedTimeRange === 'Now' || underlyingAssets.length === 0) { - setHistoricalAPYData({}); - return; - } - - const timeRangeSecondsMap: Record = { - '30D': 30 * 24 * 60 * 60, - '60D': 60 * 24 * 60 * 60, - '180D': 180 * 24 * 60 * 60, - '1Y': 365 * 24 * 60 * 60, - }; + if (selectedTimeRange === 'Now' || underlyingAssets.length === 0) { + return {}; + } + + const timeRangeSecondsMap: Record = { + '30D': 30 * 24 * 60 * 60, + '60D': 60 * 24 * 60 * 60, + '180D': 180 * 24 * 60 * 60, + '1Y': 365 * 24 * 60 * 60, + }; + + const timeRangeDaysMap: Record = { + '30D': 30, + '60D': 60, + '180D': 180, + '1Y': 365, + }; + + const timeRangeInSeconds = timeRangeSecondsMap[selectedTimeRange]; + + if (timeRangeInSeconds === undefined) { + console.error(`Invalid time range: ${selectedTimeRange}`); + return {}; + } + + const timestamp = Math.floor(Date.now() / 1000) - timeRangeInSeconds; + + const requestBodyHistory = { + query: constructIndexHistoryQuery(underlyingAssets), + variables: { timestamp }, + }; + + const requestBodyCurrent = { + query: constructIndexCurrentQuery(underlyingAssets), + }; + + const [responseHistory, responseCurrent] = await Promise.all([ + fetch(subgraphUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBodyHistory), + }), + fetch(subgraphUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBodyCurrent), + }), + ]); + + if (!responseHistory.ok || !responseCurrent.ok) { + throw new Error(`Network error: ${responseHistory.status} - ${responseHistory.statusText}`); + } + + const dataHistory = await responseHistory.json(); + const dataCurrent = await responseCurrent.json(); + + const historyByAsset: Record = {}; + const currentByAsset: Record = {}; + + const aliases = generateAliases(underlyingAssets.length); - const timeRangeDaysMap: Record = { - '30D': 30, - '60D': 60, - '180D': 180, - '1Y': 365, + underlyingAssets.forEach((_, index) => { + const alias = aliases[index]; + + const historicalEntry = dataHistory.data[alias]; + if (historicalEntry && historicalEntry.length > 0) { + const entry = historicalEntry[0]; + const assetKey = entry.reserve.underlyingAsset.toLowerCase(); + historyByAsset[assetKey] = { + underlyingAsset: assetKey, + liquidityIndex: entry.liquidityIndex, + variableBorrowIndex: entry.variableBorrowIndex, + liquidityRate: entry.liquidityRate, + variableBorrowRate: entry.variableBorrowRate, + timestamp: entry.timestamp, + }; + } + + const currentEntry = dataCurrent.data[alias]; + if (currentEntry && currentEntry.length > 0) { + const entry = currentEntry[0]; + const assetKey = entry.reserve.underlyingAsset.toLowerCase(); + currentByAsset[assetKey] = { + underlyingAsset: assetKey, + liquidityIndex: entry.liquidityIndex, + variableBorrowIndex: entry.variableBorrowIndex, + liquidityRate: entry.liquidityRate, + variableBorrowRate: entry.variableBorrowRate, + timestamp: entry.timestamp, + }; + } + }); + + const results: Record = {}; + underlyingAssets.forEach((asset) => { + const assetKey = asset.toLowerCase(); + const historical = historyByAsset[assetKey]; + const current = currentByAsset[assetKey]; + + if (historical && current) { + results[assetKey] = { + supplyAPY: calculateImpliedAPY( + Number(current.liquidityIndex), + Number(historical.liquidityIndex), + timeRangeDaysMap[selectedTimeRange] || 0 + ), + variableBorrowAPY: calculateImpliedAPY( + Number(current.variableBorrowIndex), + Number(historical.variableBorrowIndex), + timeRangeDaysMap[selectedTimeRange] || 0 + ), }; + } + }); + + return results; +}; - const timeRangeInSeconds = timeRangeSecondsMap[selectedTimeRange]; - - if (timeRangeInSeconds === undefined) { - console.error(`Invalid time range: ${selectedTimeRange}`); - setHistoricalAPYData({}); - return; - } - - const timestamp = Math.floor(Date.now() / 1000) - timeRangeInSeconds; - - try { - const requestBodyHistory = { - query: constructIndexHistoryQuery(underlyingAssets), - variables: { timestamp }, - }; - const responseHistory = await fetch(subgraphUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBodyHistory), - }); - - const requestBodyCurrent = { - query: constructIndexCurrentQuery(underlyingAssets), - }; - const responseCurrent = await fetch(subgraphUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBodyCurrent), - }); - - if (!responseHistory.ok || !responseCurrent.ok) { - throw new Error(`Network error: ${responseHistory.status} - ${responseHistory.statusText}`); - } - - const dataHistory = await responseHistory.json(); - const dataCurrent = await responseCurrent.json(); - - const historyByAsset: Record = {}; - const currentByAsset: Record = {}; - - const aliases = generateAliases(underlyingAssets.length); - - underlyingAssets.forEach((_, index) => { - const alias = aliases[index]; - - const historicalEntry = dataHistory.data[alias]; - if (historicalEntry && historicalEntry.length > 0) { - const entry = historicalEntry[0]; - const assetKey = entry.reserve.underlyingAsset.toLowerCase(); - historyByAsset[assetKey] = { - underlyingAsset: assetKey, - liquidityIndex: entry.liquidityIndex, - variableBorrowIndex: entry.variableBorrowIndex, - liquidityRate: entry.liquidityRate, - variableBorrowRate: entry.variableBorrowRate, - timestamp: entry.timestamp, - }; - } - - const currentEntry = dataCurrent.data[alias]; - if (currentEntry && currentEntry.length > 0) { - const entry = currentEntry[0]; - const assetKey = entry.reserve.underlyingAsset.toLowerCase(); - currentByAsset[assetKey] = { - underlyingAsset: assetKey, - liquidityIndex: entry.liquidityIndex, - variableBorrowIndex: entry.variableBorrowIndex, - liquidityRate: entry.liquidityRate, - variableBorrowRate: entry.variableBorrowRate, - timestamp: entry.timestamp, - }; - } - }); - - const results: Record = {}; - underlyingAssets.forEach((asset) => { - const assetKey = asset.toLowerCase(); - const historical = historyByAsset[assetKey]; - const current = currentByAsset[assetKey]; - - if (historical && current) { - results[assetKey] = { - supplyAPY: calculateImpliedAPY( - Number(current.liquidityIndex), - Number(historical.liquidityIndex), - timeRangeDaysMap[selectedTimeRange] || 0 - ), - variableBorrowAPY: calculateImpliedAPY( - Number(current.variableBorrowIndex), - Number(historical.variableBorrowIndex), - timeRangeDaysMap[selectedTimeRange] || 0 - ), - }; - } - }); - - setHistoricalAPYData(results); - } catch (error) { - console.error('Error fetching historical APY data:', error); - setHistoricalAPYData({}); - } - }; - - fetchHistoricalAPYData(); - }, [selectedTimeRange, underlyingAssets]); - - return historicalAPYData; +export const useHistoricalAPYData = ( + subgraphUrl: string, + selectedTimeRange: string, + underlyingAssets: string[] +) => { + return useQuery({ + queryKey: ['historicalAPYData', subgraphUrl, selectedTimeRange, underlyingAssets], + queryFn: () => fetchHistoricalAPYData(subgraphUrl, selectedTimeRange, underlyingAssets), + staleTime: 5 * 60 * 1000, // 5 minutes + refetchInterval: 5 * 60 * 1000, // 5 minutes + }); }; diff --git a/src/modules/markets/MarketAssetsListContainer.tsx b/src/modules/markets/MarketAssetsListContainer.tsx index 2278a5b587..f4b290ab59 100644 --- a/src/modules/markets/MarketAssetsListContainer.tsx +++ b/src/modules/markets/MarketAssetsListContainer.tsx @@ -18,11 +18,7 @@ import { useShallow } from 'zustand/shallow'; import { GENERAL } from '../../utils/mixPanelEvents'; import { GhoBanner } from './Gho/GhoBanner'; -import { - ESupportedAPYTimeRanges, - HistoricalAPYRow, - ReserveHistoricalRateTimeRange, -} from 'src/components/HistoricalAPYRow'; +import { ESupportedAPYTimeRanges, HistoricalAPYRow } from 'src/components/HistoricalAPYRow'; function shouldDisplayGhoBanner(marketTitle: string, searchTerm: string): boolean { // GHO banner is only displayed on markets where new GHO is mintable (i.e. Ethereum) @@ -68,11 +64,13 @@ export const MarketAssetsListContainer = () => { const displayGhoBanner = shouldDisplayGhoBanner(currentMarket, searchTerm); const underlyingAssets = reserves.map((a) => a.underlyingAsset); - const historicalAPYData = useHistoricalAPYData( + const { data: historicalAPYData, isLoading: isHistoricalDataLoading } = useHistoricalAPYData( currentMarketData.subgraphUrl ?? '', selectedTimeRange, underlyingAssets ); + const showHistoricalDataLoading = + selectedTimeRange !== ESupportedAPYTimeRanges.Now && isHistoricalDataLoading; const filteredData = reserves // Filter out any non-active reserves @@ -91,7 +89,7 @@ export const MarketAssetsListContainer = () => { }) // Transform the object for list to consume it .map((reserve) => { - const historicalData = historicalAPYData[reserve.underlyingAsset.toLowerCase()]; + const historicalData = historicalAPYData?.[reserve.underlyingAsset.toLowerCase()]; return { ...reserve, @@ -172,7 +170,10 @@ export const MarketAssetsListContainer = () => { )} {/* Unfrozen assets list */} - + {/* Frozen or paused assets list */} {frozenOrPausedReserves.length > 0 && ( @@ -211,7 +212,10 @@ export const MarketAssetsListContainer = () => { )} {showFrozenMarketsToggle && ( - + )} {/* Show no search results message if nothing hits in either list */} From c189a04a1bc5b4b8201bb2519399cc575661f3e2 Mon Sep 17 00:00:00 2001 From: defispartan Date: Tue, 14 Jan 2025 23:41:02 -0600 Subject: [PATCH 11/11] chore: lint --- src/components/HistoricalAPYRow.tsx | 2 +- src/hooks/useHistoricalAPYData.ts | 8 ++++---- src/modules/markets/MarketAssetsListContainer.tsx | 5 ++--- src/modules/markets/MarketAssetsListItem.tsx | 2 +- src/modules/markets/MarketAssetsListMobileItem.tsx | 2 +- src/modules/markets/index-current-query.ts | 4 ++-- src/modules/markets/index-history-query.ts | 4 ++-- src/store/protocolDataSlice.ts | 4 ++-- 8 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/components/HistoricalAPYRow.tsx b/src/components/HistoricalAPYRow.tsx index 7e9decb984..46eca782e0 100644 --- a/src/components/HistoricalAPYRow.tsx +++ b/src/components/HistoricalAPYRow.tsx @@ -16,7 +16,7 @@ export const reserveHistoricalRateTimeRangeOptions = [ ESupportedAPYTimeRanges.OneYear, ]; -export type ReserveHistoricalRateTimeRange = typeof reserveHistoricalRateTimeRangeOptions[number]; +export type ReserveHistoricalRateTimeRange = (typeof reserveHistoricalRateTimeRangeOptions)[number]; export interface TimeRangeSelectorProps { disabled?: boolean; diff --git a/src/hooks/useHistoricalAPYData.ts b/src/hooks/useHistoricalAPYData.ts index fbc50b0d14..c851f5c03d 100644 --- a/src/hooks/useHistoricalAPYData.ts +++ b/src/hooks/useHistoricalAPYData.ts @@ -20,15 +20,15 @@ interface Rates { function calculateImpliedAPY( currentLiquidityIndex: number, previousLiquidityIndex: number, - daysBetweenIndexes: number, + daysBetweenIndexes: number ): string { if (previousLiquidityIndex <= 0 || currentLiquidityIndex <= 0) { - throw new Error("Liquidity indexes must be positive values."); + throw new Error('Liquidity indexes must be positive values.'); } const growthFactor = currentLiquidityIndex / previousLiquidityIndex; const annualizedGrowthFactor = Math.pow(growthFactor, 365 / daysBetweenIndexes); - const impliedAPY = (annualizedGrowthFactor - 1); + const impliedAPY = annualizedGrowthFactor - 1; return impliedAPY.toString(); } @@ -165,6 +165,6 @@ export const useHistoricalAPYData = ( queryKey: ['historicalAPYData', subgraphUrl, selectedTimeRange, underlyingAssets], queryFn: () => fetchHistoricalAPYData(subgraphUrl, selectedTimeRange, underlyingAssets), staleTime: 5 * 60 * 1000, // 5 minutes - refetchInterval: 5 * 60 * 1000, // 5 minutes + refetchInterval: 5 * 60 * 1000, // 5 minutes }); }; diff --git a/src/modules/markets/MarketAssetsListContainer.tsx b/src/modules/markets/MarketAssetsListContainer.tsx index f4b290ab59..9c615ddef2 100644 --- a/src/modules/markets/MarketAssetsListContainer.tsx +++ b/src/modules/markets/MarketAssetsListContainer.tsx @@ -2,15 +2,15 @@ import { API_ETH_MOCK_ADDRESS } from '@aave/contract-helpers'; import { Trans } from '@lingui/macro'; import { Box, Switch, Typography, useMediaQuery, useTheme } from '@mui/material'; import { useState } from 'react'; +import { ESupportedAPYTimeRanges, HistoricalAPYRow } from 'src/components/HistoricalAPYRow'; import { ListWrapper } from 'src/components/lists/ListWrapper'; import { NoSearchResults } from 'src/components/NoSearchResults'; import { Link } from 'src/components/primitives/Link'; import { Warning } from 'src/components/primitives/Warning'; import { TitleWithSearchBar } from 'src/components/TitleWithSearchBar'; import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider'; -import MarketAssetsList from 'src/modules/markets/MarketAssetsList'; import { useHistoricalAPYData } from 'src/hooks/useHistoricalAPYData'; - +import MarketAssetsList from 'src/modules/markets/MarketAssetsList'; import { useRootStore } from 'src/store/root'; import { fetchIconSymbolAndName } from 'src/ui-config/reservePatches'; import { getGhoReserve, GHO_MINTING_MARKETS, GHO_SYMBOL } from 'src/utils/ghoUtilities'; @@ -18,7 +18,6 @@ import { useShallow } from 'zustand/shallow'; import { GENERAL } from '../../utils/mixPanelEvents'; import { GhoBanner } from './Gho/GhoBanner'; -import { ESupportedAPYTimeRanges, HistoricalAPYRow } from 'src/components/HistoricalAPYRow'; function shouldDisplayGhoBanner(marketTitle: string, searchTerm: string): boolean { // GHO banner is only displayed on markets where new GHO is mintable (i.e. Ethereum) diff --git a/src/modules/markets/MarketAssetsListItem.tsx b/src/modules/markets/MarketAssetsListItem.tsx index 509a6de573..953c0d7b39 100644 --- a/src/modules/markets/MarketAssetsListItem.tsx +++ b/src/modules/markets/MarketAssetsListItem.tsx @@ -2,6 +2,7 @@ import { ProtocolAction } from '@aave/contract-helpers'; import { Trans } from '@lingui/macro'; import { Box, Button, Typography } from '@mui/material'; import { useRouter } from 'next/router'; +import { ESupportedAPYTimeRanges } from 'src/components/HistoricalAPYRow'; import { OffboardingTooltip } from 'src/components/infoTooltips/OffboardingToolTip'; import { RenFILToolTip } from 'src/components/infoTooltips/RenFILToolTip'; import { SpkAirdropTooltip } from 'src/components/infoTooltips/SpkAirdropTooltip'; @@ -23,7 +24,6 @@ import { FormattedNumber } from '../../components/primitives/FormattedNumber'; import { Link, ROUTES } from '../../components/primitives/Link'; import { TokenIcon } from '../../components/primitives/TokenIcon'; import { ComputedReserveData } from '../../hooks/app-data-provider/useAppDataProvider'; -import { ESupportedAPYTimeRanges } from 'src/components/HistoricalAPYRow'; export const MarketAssetsListItem = ({ ...reserve }: ComputedReserveData) => { const router = useRouter(); diff --git a/src/modules/markets/MarketAssetsListMobileItem.tsx b/src/modules/markets/MarketAssetsListMobileItem.tsx index d6f9e036e1..86d77d7d2f 100644 --- a/src/modules/markets/MarketAssetsListMobileItem.tsx +++ b/src/modules/markets/MarketAssetsListMobileItem.tsx @@ -1,6 +1,7 @@ import { ProtocolAction } from '@aave/contract-helpers'; import { Trans } from '@lingui/macro'; import { Box, Button, Divider } from '@mui/material'; +import { ESupportedAPYTimeRanges } from 'src/components/HistoricalAPYRow'; import { SpkAirdropTooltip } from 'src/components/infoTooltips/SpkAirdropTooltip'; import { SuperFestTooltip } from 'src/components/infoTooltips/SuperFestTooltip'; import { VariableAPYTooltip } from 'src/components/infoTooltips/VariableAPYTooltip'; @@ -17,7 +18,6 @@ import { Link, ROUTES } from '../../components/primitives/Link'; import { Row } from '../../components/primitives/Row'; import { ComputedReserveData } from '../../hooks/app-data-provider/useAppDataProvider'; import { ListMobileItemWrapper } from '../dashboard/lists/ListMobileItemWrapper'; -import { ESupportedAPYTimeRanges } from 'src/components/HistoricalAPYRow'; export const MarketAssetsListMobileItem = ({ ...reserve }: ComputedReserveData) => { const [trackEvent, currentMarket, selectedTimeRange] = useRootStore( diff --git a/src/modules/markets/index-current-query.ts b/src/modules/markets/index-current-query.ts index cac1d2f891..b193d156e5 100644 --- a/src/modules/markets/index-current-query.ts +++ b/src/modules/markets/index-current-query.ts @@ -1,4 +1,4 @@ -import { generateAliases } from "src/utils/generateSubgraphQueryAlias"; +import { generateAliases } from 'src/utils/generateSubgraphQueryAlias'; export const constructIndexCurrentQuery = (underlyingAssets: string[]): string => { const aliases = generateAliases(underlyingAssets.length); @@ -25,7 +25,7 @@ export const constructIndexCurrentQuery = (underlyingAssets: string[]): string = return ` query IndexCurrent { - ${queries.join("\n")} + ${queries.join('\n')} } `; }; diff --git a/src/modules/markets/index-history-query.ts b/src/modules/markets/index-history-query.ts index 469bae5535..dd1c0df77e 100644 --- a/src/modules/markets/index-history-query.ts +++ b/src/modules/markets/index-history-query.ts @@ -1,4 +1,4 @@ -import { generateAliases } from "src/utils/generateSubgraphQueryAlias"; +import { generateAliases } from 'src/utils/generateSubgraphQueryAlias'; export const constructIndexHistoryQuery = (underlyingAssets: string[]): string => { const aliases = generateAliases(underlyingAssets.length); @@ -25,7 +25,7 @@ export const constructIndexHistoryQuery = (underlyingAssets: string[]): string = return ` query IndexHistory($timestamp: Int!) { - ${queries.join("\n")} + ${queries.join('\n')} } `; }; diff --git a/src/store/protocolDataSlice.ts b/src/store/protocolDataSlice.ts index 26d85d52c0..cc0c59d601 100644 --- a/src/store/protocolDataSlice.ts +++ b/src/store/protocolDataSlice.ts @@ -1,4 +1,5 @@ import { providers, utils } from 'ethers'; +import { ESupportedAPYTimeRanges } from 'src/components/HistoricalAPYRow'; import { permitByChainAndToken } from 'src/ui-config/permitConfig'; import { availableMarkets, @@ -12,7 +13,6 @@ import { CustomMarket, MarketDataType } from '../ui-config/marketsConfig'; import { NetworkConfig } from '../ui-config/networksConfig'; import { RootStore } from './root'; import { setQueryParameter } from './utils/queryParams'; -import { ESupportedAPYTimeRanges } from 'src/components/HistoricalAPYRow'; type TypePermitParams = { reserveAddress: string; @@ -47,7 +47,7 @@ export const createProtocolDataSlice: StateCreator< selectedTimeRange: ESupportedAPYTimeRanges.Now, jsonRpcProvider: (chainId) => getProvider(chainId ?? get().currentChainId), setSelectedTimeRange: (timeRange: ESupportedAPYTimeRanges) => { - set({ selectedTimeRange: timeRange }) + set({ selectedTimeRange: timeRange }); }, setCurrentMarket: (market, omitQueryParameterUpdate) => { if (!availableMarkets.includes(market as CustomMarket)) return;