diff --git a/src/components/HistoricalAPYRow.tsx b/src/components/HistoricalAPYRow.tsx new file mode 100644 index 0000000000..46eca782e0 --- /dev/null +++ b/src/components/HistoricalAPYRow.tsx @@ -0,0 +1,100 @@ +import { SxProps, Theme, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; + +const supportedHistoricalTimeRangeOptions = ['Now', '30D', '180D', '1Y'] as const; + +export enum ESupportedAPYTimeRanges { + Now = 'Now', + ThirtyDays = '30D', + OneHunredEightyDays = '180D', + OneYear = '1Y', +} + +export const reserveHistoricalRateTimeRangeOptions = [ + ESupportedAPYTimeRanges.Now, + ESupportedAPYTimeRanges.ThirtyDays, + ESupportedAPYTimeRanges.OneHunredEightyDays, + ESupportedAPYTimeRanges.OneYear, +]; + +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/components/incentives/IncentivesCard.tsx b/src/components/incentives/IncentivesCard.tsx index d6bda0c6af..de9ee7532f 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 ( @@ -49,7 +51,7 @@ export const IncentivesCard = ({ textAlign: 'center', }} > - {value.toString() !== '-1' ? ( + {value.toString() !== '-1' && value.toString() !== 'N/A' ? ( )} - - - - - + {Boolean(showIncentives) && ( + + + + + + )} ); }; diff --git a/src/hooks/useHistoricalAPYData.ts b/src/hooks/useHistoricalAPYData.ts new file mode 100644 index 0000000000..c851f5c03d --- /dev/null +++ b/src/hooks/useHistoricalAPYData.ts @@ -0,0 +1,170 @@ +import { useQuery } from '@tanstack/react-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; + 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(); +} + +const fetchHistoricalAPYData = async ( + subgraphUrl: string, + selectedTimeRange: string, + underlyingAssets: string[] +) => { + 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); + + 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; +}; + +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 7182ef6c17..9c615ddef2 100644 --- a/src/modules/markets/MarketAssetsListContainer.tsx +++ b/src/modules/markets/MarketAssetsListContainer.tsx @@ -2,12 +2,14 @@ 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 { 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'; @@ -36,12 +38,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(''); @@ -51,12 +62,21 @@ export const MarketAssetsListContainer = () => { const ghoReserve = getGhoReserve(reserves); const displayGhoBanner = shouldDisplayGhoBanner(currentMarket, searchTerm); + const underlyingAssets = reserves.map((a) => a.underlyingAsset); + 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 .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(); @@ -67,15 +87,32 @@ 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); @@ -91,15 +128,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 && ( @@ -109,7 +169,10 @@ export const MarketAssetsListContainer = () => { )} {/* Unfrozen assets list */} - + {/* Frozen or paused assets list */} {frozenOrPausedReserves.length > 0 && ( @@ -148,7 +211,10 @@ export const MarketAssetsListContainer = () => { )} {showFrozenMarketsToggle && ( - + )} {/* Show no search results message if nothing hits in either list */} diff --git a/src/modules/markets/MarketAssetsListItem.tsx b/src/modules/markets/MarketAssetsListItem.tsx index be2af610ad..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'; @@ -26,8 +27,8 @@ import { ComputedReserveData } from '../../hooks/app-data-provider/useAppDataPro 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..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'; @@ -19,8 +20,8 @@ import { ComputedReserveData } from '../../hooks/app-data-provider/useAppDataPro import { ListMobileItemWrapper } from '../dashboard/lists/ListMobileItemWrapper'; 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/modules/markets/index-current-query.ts b/src/modules/markets/index-current-query.ts new file mode 100644 index 0000000000..b193d156e5 --- /dev/null +++ b/src/modules/markets/index-current-query.ts @@ -0,0 +1,31 @@ +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 new file mode 100644 index 0000000000..dd1c0df77e --- /dev/null +++ b/src/modules/markets/index-history-query.ts @@ -0,0 +1,31 @@ +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')} + } + `; +}; diff --git a/src/store/protocolDataSlice.ts b/src/store/protocolDataSlice.ts index 3247c6b503..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, @@ -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]; 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; +};