diff --git a/.changeset/eighty-results-drop.md b/.changeset/eighty-results-drop.md new file mode 100644 index 0000000000..a0e7cfec35 --- /dev/null +++ b/.changeset/eighty-results-drop.md @@ -0,0 +1,5 @@ +--- +"@venusprotocol/evm": minor +--- + +Added rates tab diff --git a/apps/evm/src/clients/api/index.ts b/apps/evm/src/clients/api/index.ts index ad8a8686d8..3646329983 100644 --- a/apps/evm/src/clients/api/index.ts +++ b/apps/evm/src/clients/api/index.ts @@ -335,6 +335,10 @@ export * from './queries/getRiskDashboardLiquidationsDistribution'; export * from './queries/getRiskDashboardLiquidationsDistribution/useGetRiskDashboardLiquidationsDistribution'; export * from './queries/getRiskDashboardMarketSnapshots'; export * from './queries/getRiskDashboardMarketSnapshots/useGetRiskDashboardMarketSnapshots'; +export * from './queries/getRiskDashboardRatesHistory'; +export * from './queries/getRiskDashboardRatesHistory/useGetRiskDashboardRatesHistory'; +export * from './queries/getRiskDashboardStablecoinRates'; +export * from './queries/getRiskDashboardStablecoinRates/useGetRiskDashboardStablecoinRates'; export * from './queries/getRiskDashboardWalletAggregates'; export * from './queries/getRiskDashboardWalletAggregates/useGetRiskDashboardWalletAggregates'; export * from './queries/getRiskDashboardTopWallets'; diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardRatesHistory/index.ts b/apps/evm/src/clients/api/queries/getRiskDashboardRatesHistory/index.ts new file mode 100644 index 0000000000..1c196b4f66 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardRatesHistory/index.ts @@ -0,0 +1,60 @@ +import { VError } from 'libs/errors'; +import type { ChainId } from 'types'; +import { restService } from 'utilities'; +import type { Address } from 'viem'; +import type { RiskDashboardMarketSnapshotKind } from '../getRiskDashboardMarketSnapshots'; + +export interface ApiRiskDashboardRatesHistoryMarketPoint { + marketAddress: Address; + underlyingAddress: Address | null; + supplyRatePerBlockMantissa: string; + borrowRatePerBlockMantissa: string; + supplyApy: number; + borrowApy: number; + supplyUsdCents: string; + borrowsUsdCents: string; +} + +export interface ApiRiskDashboardRatesHistorySlot { + blockNumber: string; + blockTimestamp: string; + byMarket: ApiRiskDashboardRatesHistoryMarketPoint[]; +} + +export interface GetRiskDashboardRatesHistoryInput { + chainId: ChainId; + topTokens?: boolean; +} + +export interface GetRiskDashboardRatesHistoryResponse { + chainId: string; + kind: RiskDashboardMarketSnapshotKind; + series: ApiRiskDashboardRatesHistorySlot[]; +} + +export async function getRiskDashboardRatesHistory({ + chainId, + topTokens, +}: GetRiskDashboardRatesHistoryInput) { + const response = await restService({ + endpoint: '/risk-dashboard/rates-history', + method: 'GET', + params: { chainId, ...(topTokens ? { topTokens: true } : {}) }, + }); + + const payload = response.data; + + if (payload && 'error' in payload) { + throw new VError({ + type: 'unexpected', + code: 'somethingWentWrong', + data: { exception: payload.error }, + }); + } + + if (!payload) { + throw new VError({ type: 'unexpected', code: 'somethingWentWrong' }); + } + + return payload; +} diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardRatesHistory/useGetRiskDashboardRatesHistory.ts b/apps/evm/src/clients/api/queries/getRiskDashboardRatesHistory/useGetRiskDashboardRatesHistory.ts new file mode 100644 index 0000000000..ea49dfef34 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardRatesHistory/useGetRiskDashboardRatesHistory.ts @@ -0,0 +1,32 @@ +import { type QueryObserverOptions, useQuery } from '@tanstack/react-query'; + +import FunctionKey from 'constants/functionKey'; +import { useChainId } from 'libs/wallet'; +import type { ChainId } from 'types'; +import { type GetRiskDashboardRatesHistoryResponse, getRiskDashboardRatesHistory } from '.'; + +export type UseGetRiskDashboardRatesHistoryQueryKey = [ + FunctionKey.GET_RISK_DASHBOARD_RATES_HISTORY, + { chainId: ChainId; topTokens?: boolean }, +]; + +type Options = QueryObserverOptions< + GetRiskDashboardRatesHistoryResponse, + Error, + GetRiskDashboardRatesHistoryResponse, + GetRiskDashboardRatesHistoryResponse, + UseGetRiskDashboardRatesHistoryQueryKey +>; + +export const useGetRiskDashboardRatesHistory = ( + { topTokens }: { topTokens?: boolean } = {}, + options?: Partial, +) => { + const { chainId } = useChainId(); + + return useQuery({ + queryKey: [FunctionKey.GET_RISK_DASHBOARD_RATES_HISTORY, { chainId, topTokens }], + queryFn: () => getRiskDashboardRatesHistory({ chainId, topTokens }), + ...options, + }); +}; diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardStablecoinRates/index.ts b/apps/evm/src/clients/api/queries/getRiskDashboardStablecoinRates/index.ts new file mode 100644 index 0000000000..627eb9f822 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardStablecoinRates/index.ts @@ -0,0 +1,62 @@ +import { VError } from 'libs/errors'; +import type { ChainId } from 'types'; +import { restService } from 'utilities'; +import type { Address } from 'viem'; +import type { RiskDashboardMarketSnapshotKind } from '../getRiskDashboardMarketSnapshots'; + +export interface ApiRiskDashboardStablecoinRatesAssetPoint { + marketAddress: Address; + underlyingAddress: Address; + supplyUsdCents: string; + borrowsUsdCents: string; + supplyShare: number; + borrowShare: number; +} + +export interface ApiRiskDashboardStablecoinRatesSlot { + blockNumber: string; + blockTimestamp: string; + supplyRatePerBlockMantissa: string; + borrowRatePerBlockMantissa: string; + supplyApy: number; + borrowApy: number; + totalSupplyUsdCents: string; + totalBorrowsUsdCents: string; + byAsset: ApiRiskDashboardStablecoinRatesAssetPoint[]; +} + +export interface GetRiskDashboardStablecoinRatesInput { + chainId: ChainId; +} + +export interface GetRiskDashboardStablecoinRatesResponse { + chainId: string; + kind: RiskDashboardMarketSnapshotKind; + series: ApiRiskDashboardStablecoinRatesSlot[]; +} + +export async function getRiskDashboardStablecoinRates({ + chainId, +}: GetRiskDashboardStablecoinRatesInput) { + const response = await restService({ + endpoint: '/risk-dashboard/stablecoin-rates', + method: 'GET', + params: { chainId }, + }); + + const payload = response.data; + + if (payload && 'error' in payload) { + throw new VError({ + type: 'unexpected', + code: 'somethingWentWrong', + data: { exception: payload.error }, + }); + } + + if (!payload) { + throw new VError({ type: 'unexpected', code: 'somethingWentWrong' }); + } + + return payload; +} diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardStablecoinRates/useGetRiskDashboardStablecoinRates.ts b/apps/evm/src/clients/api/queries/getRiskDashboardStablecoinRates/useGetRiskDashboardStablecoinRates.ts new file mode 100644 index 0000000000..6094127cf7 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardStablecoinRates/useGetRiskDashboardStablecoinRates.ts @@ -0,0 +1,29 @@ +import { type QueryObserverOptions, useQuery } from '@tanstack/react-query'; + +import FunctionKey from 'constants/functionKey'; +import { useChainId } from 'libs/wallet'; +import type { ChainId } from 'types'; +import { type GetRiskDashboardStablecoinRatesResponse, getRiskDashboardStablecoinRates } from '.'; + +export type UseGetRiskDashboardStablecoinRatesQueryKey = [ + FunctionKey.GET_RISK_DASHBOARD_STABLECOIN_RATES, + { chainId: ChainId }, +]; + +type Options = QueryObserverOptions< + GetRiskDashboardStablecoinRatesResponse, + Error, + GetRiskDashboardStablecoinRatesResponse, + GetRiskDashboardStablecoinRatesResponse, + UseGetRiskDashboardStablecoinRatesQueryKey +>; + +export const useGetRiskDashboardStablecoinRates = (options?: Partial) => { + const { chainId } = useChainId(); + + return useQuery({ + queryKey: [FunctionKey.GET_RISK_DASHBOARD_STABLECOIN_RATES, { chainId }], + queryFn: () => getRiskDashboardStablecoinRates({ chainId }), + ...options, + }); +}; diff --git a/apps/evm/src/constants/functionKey.ts b/apps/evm/src/constants/functionKey.ts index f455ca98e6..c4f1f38110 100644 --- a/apps/evm/src/constants/functionKey.ts +++ b/apps/evm/src/constants/functionKey.ts @@ -109,6 +109,8 @@ enum FunctionKey { GET_RISK_DASHBOARD_LIQUIDATIONS_SUMMARY = 'GET_RISK_DASHBOARD_LIQUIDATIONS_SUMMARY', GET_RISK_DASHBOARD_LIQUIDATIONS_DAILY = 'GET_RISK_DASHBOARD_LIQUIDATIONS_DAILY', GET_RISK_DASHBOARD_LIQUIDATIONS_DISTRIBUTION = 'GET_RISK_DASHBOARD_LIQUIDATIONS_DISTRIBUTION', + GET_RISK_DASHBOARD_RATES_HISTORY = 'GET_RISK_DASHBOARD_RATES_HISTORY', + GET_RISK_DASHBOARD_STABLECOIN_RATES = 'GET_RISK_DASHBOARD_STABLECOIN_RATES', GET_IMAGE_ACCENT_COLOR = 'GET_IMAGE_ACCENT_COLOR', } diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index b4d0899e2b..90cdf514e5 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1829,6 +1829,46 @@ "title": "Markets", "unavailable": "Markets data unavailable." }, + "rates": { + "apyChart": { + "borrows": { + "noData": "No historical snapshots yet.", + "title": "Borrow APY by Asset over Time", + "unavailable": "Borrow APY unavailable." + }, + "supply": { + "noData": "No historical snapshots yet.", + "title": "Supply APY by Asset over Time", + "unavailable": "Supply APY unavailable." + } + }, + "dominance": { + "borrows": { + "noData": "No historical snapshots yet.", + "title": "Stablecoin Borrow Dominance over Time", + "unavailable": "Borrow dominance unavailable." + }, + "supply": { + "noData": "No historical snapshots yet.", + "title": "Stablecoin Supply Dominance over Time", + "unavailable": "Supply dominance unavailable." + } + }, + "stablecoinRates": { + "borrowLabel": "Borrow APY", + "noData": "No historical snapshots yet.", + "supplyLabel": "Supply APY", + "title": "Weighted Average Stablecoin Rates over Time", + "unavailable": "Stablecoin rates unavailable." + }, + "totals": { + "borrowLabel": "Total Borrows", + "noData": "No historical snapshots yet.", + "supplyLabel": "Total Supply", + "title": "Total Stablecoin Supply vs Borrows over Time", + "unavailable": "Stablecoin totals unavailable." + } + }, "riskParametersTable": { "columns": { "asset": "Asset", @@ -1850,6 +1890,7 @@ "liquidations": "Liquidations", "markets": "Markets", "overview": "Overview", + "rates": "Rates", "wallets": "Wallets" }, "title": "Venus Stats", diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index beec307d18..cf57861caf 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1829,6 +1829,46 @@ "title": "TRANSLATION NEEDED", "unavailable": "TRANSLATION NEEDED" }, + "rates": { + "apyChart": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "dominance": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "stablecoinRates": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "totals": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, "riskParametersTable": { "columns": { "asset": "TRANSLATION NEEDED", @@ -1850,6 +1890,7 @@ "liquidations": "TRANSLATION NEEDED", "markets": "TRANSLATION NEEDED", "overview": "TRANSLATION NEEDED", + "rates": "TRANSLATION NEEDED", "wallets": "TRANSLATION NEEDED" }, "title": "Venus 統計", diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index 79753450f5..9c18fa60d5 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1829,6 +1829,46 @@ "title": "TRANSLATION NEEDED", "unavailable": "TRANSLATION NEEDED" }, + "rates": { + "apyChart": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "dominance": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "stablecoinRates": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "totals": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, "riskParametersTable": { "columns": { "asset": "TRANSLATION NEEDED", @@ -1850,6 +1890,7 @@ "liquidations": "TRANSLATION NEEDED", "markets": "TRANSLATION NEEDED", "overview": "TRANSLATION NEEDED", + "rates": "TRANSLATION NEEDED", "wallets": "TRANSLATION NEEDED" }, "title": "สถิติ Venus", diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index fa707bc013..e909d643cf 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1829,6 +1829,46 @@ "title": "TRANSLATION NEEDED", "unavailable": "TRANSLATION NEEDED" }, + "rates": { + "apyChart": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "dominance": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "stablecoinRates": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "totals": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, "riskParametersTable": { "columns": { "asset": "TRANSLATION NEEDED", @@ -1850,6 +1890,7 @@ "liquidations": "TRANSLATION NEEDED", "markets": "TRANSLATION NEEDED", "overview": "TRANSLATION NEEDED", + "rates": "TRANSLATION NEEDED", "wallets": "TRANSLATION NEEDED" }, "title": "Venus İstatistikleri", diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index f9855f7161..aafd9ee145 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1829,6 +1829,46 @@ "title": "TRANSLATION NEEDED", "unavailable": "TRANSLATION NEEDED" }, + "rates": { + "apyChart": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "dominance": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "stablecoinRates": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "totals": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, "riskParametersTable": { "columns": { "asset": "TRANSLATION NEEDED", @@ -1850,6 +1890,7 @@ "liquidations": "TRANSLATION NEEDED", "markets": "TRANSLATION NEEDED", "overview": "TRANSLATION NEEDED", + "rates": "TRANSLATION NEEDED", "wallets": "TRANSLATION NEEDED" }, "title": "Thống kê Venus", diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index 01e0f0893a..ba2866525c 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1829,6 +1829,46 @@ "title": "TRANSLATION NEEDED", "unavailable": "TRANSLATION NEEDED" }, + "rates": { + "apyChart": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "dominance": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "stablecoinRates": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "totals": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, "riskParametersTable": { "columns": { "asset": "TRANSLATION NEEDED", @@ -1850,6 +1890,7 @@ "liquidations": "TRANSLATION NEEDED", "markets": "TRANSLATION NEEDED", "overview": "TRANSLATION NEEDED", + "rates": "TRANSLATION NEEDED", "wallets": "TRANSLATION NEEDED" }, "title": "Venus 统计", diff --git a/apps/evm/src/libs/translations/translations/zh-Hant.json b/apps/evm/src/libs/translations/translations/zh-Hant.json index 7aaff96bd7..ef0e585f61 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1829,6 +1829,46 @@ "title": "TRANSLATION NEEDED", "unavailable": "TRANSLATION NEEDED" }, + "rates": { + "apyChart": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "dominance": { + "borrows": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "supply": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, + "stablecoinRates": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + }, + "totals": { + "borrowLabel": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "supplyLabel": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED" + } + }, "riskParametersTable": { "columns": { "asset": "TRANSLATION NEEDED", @@ -1850,6 +1890,7 @@ "liquidations": "TRANSLATION NEEDED", "markets": "TRANSLATION NEEDED", "overview": "TRANSLATION NEEDED", + "rates": "TRANSLATION NEEDED", "wallets": "TRANSLATION NEEDED" }, "title": "Venus 統計", diff --git a/apps/evm/src/pages/Stats/Rates/RatesApyChart/index.tsx b/apps/evm/src/pages/Stats/Rates/RatesApyChart/index.tsx new file mode 100644 index 0000000000..445e26eac9 --- /dev/null +++ b/apps/evm/src/pages/Stats/Rates/RatesApyChart/index.tsx @@ -0,0 +1,222 @@ +import { theme } from '@venusprotocol/ui'; +import { + type ApiRiskDashboardRatesHistorySlot, + useGetRiskDashboardRatesHistory, +} from 'clients/api'; +import { Card, Spinner } from 'components'; +import { useGetVTokens } from 'libs/tokens/hooks/useGetVTokens'; +import { useTranslation } from 'libs/translations'; +import { useMemo } from 'react'; +import { + CartesianGrid, + Line, + LineChart as RCLineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { + convertDecimalToPercentage, + formatPercentageToReadableValue, + formatToReadableDate, +} from 'utilities'; +import { getAddress } from 'viem'; +import { useMarketColors } from '../../useMarketColors'; + +type Metric = 'supply' | 'borrows'; + +interface ChartDatum { + timestamp: number; + blockNumber: string; + [marketAddress: string]: number | string; +} + +const readPointApyPercentage = (point: { supplyApy: number; borrowApy: number }, metric: Metric) => + convertDecimalToPercentage(metric === 'supply' ? point.supplyApy : point.borrowApy); + +const collectMarketsByLatestApy = (series: ApiRiskDashboardRatesHistorySlot[], metric: Metric) => { + const latestApyByMarket = new Map(); + for (const slot of series) { + for (const point of slot.byMarket) { + latestApyByMarket.set(getAddress(point.marketAddress), readPointApyPercentage(point, metric)); + } + } + return [...latestApyByMarket.entries()] + .sort((entryA, entryB) => entryB[1] - entryA[1]) + .map(([market]) => market); +}; + +const buildChartData = (series: ApiRiskDashboardRatesHistorySlot[], metric: Metric): ChartDatum[] => + series.map(slot => { + const datum: ChartDatum = { + timestamp: new Date(slot.blockTimestamp).getTime(), + blockNumber: slot.blockNumber, + }; + for (const point of slot.byMarket) { + datum[getAddress(point.marketAddress)] = readPointApyPercentage(point, metric); + } + return datum; + }); + +interface TooltipPayloadItem { + dataKey: unknown; + value: number; + color: string; + payload: ChartDatum; +} + +const ApyTooltip: React.FC<{ + active?: boolean; + payload?: TooltipPayloadItem[]; + symbolByMarket: Record; +}> = ({ active, payload, symbolByMarket }) => { + const { t } = useTranslation(); + + if (!active || !payload || payload.length === 0) { + return null; + } + const datum = payload[0].payload; + + const breakdown = payload + .map(item => { + if (typeof item.dataKey !== 'string') { + return null; + } + return { key: item.dataKey, value: item.value, color: item.color }; + }) + .filter((item): item is { key: string; value: number; color: string } => !!item) + .sort((itemA, itemB) => itemB.value - itemA.value); + + return ( +
+
+ + {formatToReadableDate({ timestampMs: datum.timestamp, t })} + +
+ + {breakdown.map(item => ( +
+
+ + + {symbolByMarket[item.key] ?? item.key.slice(0, 8)} + +
+ + {formatPercentageToReadableValue(item.value)} + +
+ ))} +
+ ); +}; + +export interface RatesApyChartProps { + metric: Metric; +} + +export const RatesApyChart: React.FC = ({ metric }) => { + const { t } = useTranslation(); + const { data, isLoading, isError } = useGetRiskDashboardRatesHistory({ topTokens: true }); + const vTokens = useGetVTokens(); + const { colorByMarket } = useMarketColors(); + + const symbolByMarket = useMemo(() => { + const map: Record = {}; + for (const vToken of vTokens) { + map[getAddress(vToken.address)] = vToken.underlyingToken.symbol; + } + return map; + }, [vTokens]); + + const markets = useMemo( + () => (data ? collectMarketsByLatestApy(data.series, metric) : []), + [data, metric], + ); + + const chartData = useMemo( + () => (data ? buildChartData(data.series, metric) : []), + [data, metric], + ); + + // t('statsPage.rates.apyChart.supply.title') + // t('statsPage.rates.apyChart.supply.unavailable') + // t('statsPage.rates.apyChart.supply.noData') + // t('statsPage.rates.apyChart.borrows.title') + // t('statsPage.rates.apyChart.borrows.unavailable') + // t('statsPage.rates.apyChart.borrows.noData') + const title = t(`statsPage.rates.apyChart.${metric}.title`); + const unavailableMessage = t(`statsPage.rates.apyChart.${metric}.unavailable`); + const noDataMessage = t(`statsPage.rates.apyChart.${metric}.noData`); + + return ( + +

{title}

+ +
+ {isLoading ? ( + + ) : isError || !data ? ( +

{unavailableMessage}

+ ) : chartData.length === 0 ? ( +

{noDataMessage}

+ ) : ( + + + + + formatToReadableDate({ timestampMs: value, t })} + tick={{ fill: theme.colors.grey, fontSize: 11 }} + /> + + ( + + {formatPercentageToReadableValue(payload.value)} + + )} + /> + + } + /> + + {markets.map(market => ( + + ))} + + + )} +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/Rates/StablecoinDominanceChart/index.tsx b/apps/evm/src/pages/Stats/Rates/StablecoinDominanceChart/index.tsx new file mode 100644 index 0000000000..e70ece78ed --- /dev/null +++ b/apps/evm/src/pages/Stats/Rates/StablecoinDominanceChart/index.tsx @@ -0,0 +1,234 @@ +import { theme } from '@venusprotocol/ui'; +import { + type ApiRiskDashboardStablecoinRatesSlot, + useGetRiskDashboardStablecoinRates, +} from 'clients/api'; +import { Card, Spinner } from 'components'; +import { useGetVTokens } from 'libs/tokens/hooks/useGetVTokens'; +import { useTranslation } from 'libs/translations'; +import { useMemo } from 'react'; +import { + Area, + CartesianGrid, + AreaChart as RCAreaChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { + convertDecimalToPercentage, + formatPercentageToReadableValue, + formatToReadableDate, +} from 'utilities'; +import { getAddress } from 'viem'; +import { useMarketColors } from '../../useMarketColors'; + +type Metric = 'supply' | 'borrows'; + +interface ChartDatum { + timestamp: number; + blockNumber: string; + [marketAddress: string]: number | string; +} + +const readAssetShare = (asset: { supplyShare: number; borrowShare: number }, metric: Metric) => + metric === 'supply' ? asset.supplyShare : asset.borrowShare; + +const collectMarketsByLatestShare = ( + series: ApiRiskDashboardStablecoinRatesSlot[], + metric: Metric, +) => { + const latestShareByMarket = new Map(); + for (const slot of series) { + for (const asset of slot.byAsset) { + latestShareByMarket.set(getAddress(asset.marketAddress), readAssetShare(asset, metric)); + } + } + return [...latestShareByMarket.entries()] + .sort((entryA, entryB) => entryB[1] - entryA[1]) + .map(([market]) => market); +}; + +const buildChartData = ( + series: ApiRiskDashboardStablecoinRatesSlot[], + metric: Metric, +): ChartDatum[] => + series.map(slot => { + const datum: ChartDatum = { + timestamp: new Date(slot.blockTimestamp).getTime(), + blockNumber: slot.blockNumber, + }; + for (const asset of slot.byAsset) { + datum[getAddress(asset.marketAddress)] = readAssetShare(asset, metric); + } + return datum; + }); + +interface TooltipPayloadItem { + dataKey: unknown; + value: number; + color: string; + payload: ChartDatum; +} + +const DominanceTooltip: React.FC<{ + active?: boolean; + payload?: TooltipPayloadItem[]; + symbolByMarket: Record; +}> = ({ active, payload, symbolByMarket }) => { + const { t } = useTranslation(); + + if (!active || !payload || payload.length === 0) { + return null; + } + const datum = payload[0].payload; + + const breakdown = payload + .map(item => { + if (typeof item.dataKey !== 'string' || item.value === 0) { + return null; + } + return { key: item.dataKey, value: item.value, color: item.color }; + }) + .filter((item): item is { key: string; value: number; color: string } => !!item) + .sort((itemA, itemB) => itemB.value - itemA.value); + + return ( +
+
+ + {formatToReadableDate({ timestampMs: datum.timestamp, t })} + +
+ + {breakdown.map(item => ( +
+
+ + + {symbolByMarket[item.key] ?? item.key.slice(0, 8)} + +
+ + {formatPercentageToReadableValue(convertDecimalToPercentage(item.value))} + +
+ ))} +
+ ); +}; + +export interface StablecoinDominanceChartProps { + metric: Metric; +} + +export const StablecoinDominanceChart: React.FC = ({ metric }) => { + const { t } = useTranslation(); + const { data, isLoading, isError } = useGetRiskDashboardStablecoinRates(); + const vTokens = useGetVTokens(); + const { colorByMarket } = useMarketColors(); + + const symbolByMarket = useMemo(() => { + const map: Record = {}; + for (const vToken of vTokens) { + map[getAddress(vToken.address)] = vToken.underlyingToken.symbol; + } + return map; + }, [vTokens]); + + const markets = useMemo( + () => (data ? collectMarketsByLatestShare(data.series, metric) : []), + [data, metric], + ); + + const chartData = useMemo( + () => (data ? buildChartData(data.series, metric) : []), + [data, metric], + ); + + // t('statsPage.rates.dominance.supply.title') + // t('statsPage.rates.dominance.supply.unavailable') + // t('statsPage.rates.dominance.supply.noData') + // t('statsPage.rates.dominance.borrows.title') + // t('statsPage.rates.dominance.borrows.unavailable') + // t('statsPage.rates.dominance.borrows.noData') + const title = t(`statsPage.rates.dominance.${metric}.title`); + const unavailableMessage = t(`statsPage.rates.dominance.${metric}.unavailable`); + const noDataMessage = t(`statsPage.rates.dominance.${metric}.noData`); + + return ( + +

{title}

+ +
+ {isLoading ? ( + + ) : isError || !data ? ( +

{unavailableMessage}

+ ) : chartData.length === 0 ? ( +

{noDataMessage}

+ ) : ( + + + + + formatToReadableDate({ timestampMs: value, t })} + tick={{ fill: theme.colors.grey, fontSize: 11 }} + /> + + ( + + {formatPercentageToReadableValue(convertDecimalToPercentage(payload.value))} + + )} + /> + + } + /> + + {markets.map(market => ( + + ))} + + + )} +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/Rates/StablecoinRatesChart/index.tsx b/apps/evm/src/pages/Stats/Rates/StablecoinRatesChart/index.tsx new file mode 100644 index 0000000000..7f50fe2bbf --- /dev/null +++ b/apps/evm/src/pages/Stats/Rates/StablecoinRatesChart/index.tsx @@ -0,0 +1,167 @@ +import { theme } from '@venusprotocol/ui'; +import { + type ApiRiskDashboardStablecoinRatesSlot, + useGetRiskDashboardStablecoinRates, +} from 'clients/api'; +import { Card, Spinner } from 'components'; +import { useTranslation } from 'libs/translations'; +import { useMemo } from 'react'; +import { + CartesianGrid, + Line, + LineChart as RCLineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { + convertDecimalToPercentage, + formatPercentageToReadableValue, + formatToReadableDate, +} from 'utilities'; + +interface ChartDatum { + timestamp: number; + blockNumber: string; + supplyApyPercentage: number; + borrowApyPercentage: number; +} + +const buildChartData = (series: ApiRiskDashboardStablecoinRatesSlot[]): ChartDatum[] => + series.map(slot => ({ + timestamp: new Date(slot.blockTimestamp).getTime(), + blockNumber: slot.blockNumber, + supplyApyPercentage: convertDecimalToPercentage(slot.supplyApy), + borrowApyPercentage: convertDecimalToPercentage(slot.borrowApy), + })); + +interface TooltipPayloadItem { + payload: ChartDatum; +} + +const RatesTooltip: React.FC<{ + active?: boolean; + payload?: TooltipPayloadItem[]; +}> = ({ active, payload }) => { + const { t } = useTranslation(); + + if (!active || !payload || payload.length === 0) { + return null; + } + const datum = payload[0].payload; + + return ( +
+
+ + {formatToReadableDate({ timestampMs: datum.timestamp, t })} + +
+
+
+ + + {t('statsPage.rates.stablecoinRates.supplyLabel')} + +
+ + {formatPercentageToReadableValue(datum.supplyApyPercentage)} + +
+
+
+ + + {t('statsPage.rates.stablecoinRates.borrowLabel')} + +
+ + {formatPercentageToReadableValue(datum.borrowApyPercentage)} + +
+
+ ); +}; + +export const StablecoinRatesChart: React.FC = () => { + const { t } = useTranslation(); + const { data, isLoading, isError } = useGetRiskDashboardStablecoinRates(); + + const chartData = useMemo(() => (data ? buildChartData(data.series) : []), [data]); + + return ( + +

{t('statsPage.rates.stablecoinRates.title')}

+ +
+ {isLoading ? ( + + ) : isError || !data ? ( +

{t('statsPage.rates.stablecoinRates.unavailable')}

+ ) : chartData.length === 0 ? ( +

{t('statsPage.rates.stablecoinRates.noData')}

+ ) : ( + + + + + formatToReadableDate({ timestampMs: value, t })} + tick={{ fill: theme.colors.grey, fontSize: 11 }} + /> + + ( + + {formatPercentageToReadableValue(payload.value)} + + )} + /> + + } + /> + + + + + + )} +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/Rates/StablecoinTotalsChart/index.tsx b/apps/evm/src/pages/Stats/Rates/StablecoinTotalsChart/index.tsx new file mode 100644 index 0000000000..13289893ee --- /dev/null +++ b/apps/evm/src/pages/Stats/Rates/StablecoinTotalsChart/index.tsx @@ -0,0 +1,168 @@ +import { theme } from '@venusprotocol/ui'; +import { + type ApiRiskDashboardStablecoinRatesSlot, + useGetRiskDashboardStablecoinRates, +} from 'clients/api'; +import { Card, Spinner } from 'components'; +import { useTranslation } from 'libs/translations'; +import { useMemo } from 'react'; +import { + CartesianGrid, + Line, + LineChart as RCLineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { formatCentsToReadableValue, formatToReadableDate } from 'utilities'; + +interface ChartDatum { + timestamp: number; + blockNumber: string; + supplyDollars: number; + borrowsDollars: number; + totalSupplyUsdCents: string; + totalBorrowsUsdCents: string; +} + +const toDollars = (cents: string) => Number(cents) / 100; + +const formatDollarsAxis = (dollars: number) => + formatCentsToReadableValue({ value: Math.round(Math.abs(dollars) * 100) }); + +const buildChartData = (series: ApiRiskDashboardStablecoinRatesSlot[]): ChartDatum[] => + series.map(slot => ({ + timestamp: new Date(slot.blockTimestamp).getTime(), + blockNumber: slot.blockNumber, + supplyDollars: toDollars(slot.totalSupplyUsdCents), + borrowsDollars: toDollars(slot.totalBorrowsUsdCents), + totalSupplyUsdCents: slot.totalSupplyUsdCents, + totalBorrowsUsdCents: slot.totalBorrowsUsdCents, + })); + +interface TooltipPayloadItem { + payload: ChartDatum; +} + +const TotalsTooltip: React.FC<{ + active?: boolean; + payload?: TooltipPayloadItem[]; +}> = ({ active, payload }) => { + const { t } = useTranslation(); + + if (!active || !payload || payload.length === 0) { + return null; + } + const datum = payload[0].payload; + + return ( +
+
+ + {formatToReadableDate({ timestampMs: datum.timestamp, t })} + +
+
+
+ + {t('statsPage.rates.totals.supplyLabel')} +
+ + {formatCentsToReadableValue({ value: datum.totalSupplyUsdCents })} + +
+
+
+ + {t('statsPage.rates.totals.borrowLabel')} +
+ + {formatCentsToReadableValue({ value: datum.totalBorrowsUsdCents })} + +
+
+ ); +}; + +export const StablecoinTotalsChart: React.FC = () => { + const { t } = useTranslation(); + const { data, isLoading, isError } = useGetRiskDashboardStablecoinRates(); + + const chartData = useMemo(() => (data ? buildChartData(data.series) : []), [data]); + + return ( + +

{t('statsPage.rates.totals.title')}

+ +
+ {isLoading ? ( + + ) : isError || !data ? ( +

{t('statsPage.rates.totals.unavailable')}

+ ) : chartData.length === 0 ? ( +

{t('statsPage.rates.totals.noData')}

+ ) : ( + + + + + formatToReadableDate({ timestampMs: value, t })} + tick={{ fill: theme.colors.grey, fontSize: 11 }} + /> + + ( + + {formatDollarsAxis(payload.value)} + + )} + /> + + } + /> + + + + + + )} +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/Rates/index.tsx b/apps/evm/src/pages/Stats/Rates/index.tsx new file mode 100644 index 0000000000..5f37db5ea7 --- /dev/null +++ b/apps/evm/src/pages/Stats/Rates/index.tsx @@ -0,0 +1,19 @@ +import { RatesApyChart } from './RatesApyChart'; +import { StablecoinDominanceChart } from './StablecoinDominanceChart'; +import { StablecoinRatesChart } from './StablecoinRatesChart'; +import { StablecoinTotalsChart } from './StablecoinTotalsChart'; + +export const Rates: React.FC = () => ( +
+
+ + +
+ +
+ + +
+ +
+); diff --git a/apps/evm/src/pages/Stats/index.tsx b/apps/evm/src/pages/Stats/index.tsx index bec0250fa5..91713cdb41 100644 --- a/apps/evm/src/pages/Stats/index.tsx +++ b/apps/evm/src/pages/Stats/index.tsx @@ -2,12 +2,13 @@ import { Page, Tabs } from 'components'; import { useTranslation } from 'libs/translations'; import { EModeTable } from './EModeTable'; import { Header } from './Header'; -import { Liquidations } from './Liquidations'; import { HistoricalDominanceChart } from './HistoricalDominanceChart'; import { HistoricalLiquidityChart } from './HistoricalLiquidityChart'; import { HistoricalMarketChart } from './HistoricalMarketChart'; +import { Liquidations } from './Liquidations'; import { MarketKpis } from './MarketKpis'; import { MarketsTable } from './MarketsTable'; +import { Rates } from './Rates'; import { RiskParametersTable } from './RiskParametersTable'; import { TopWallets } from './TopWallets'; import { TransactionsVolume } from './TransactionsVolume'; @@ -54,7 +55,12 @@ const Stats: React.FC = () => { { id: 'overview', title: t('statsPage.tabs.overview'), content: }, { id: 'markets', title: t('statsPage.tabs.markets'), content: }, { id: 'wallets', title: t('statsPage.tabs.wallets'), content: null }, - { id: 'liquidations', title: t('statsPage.tabs.liquidations'), content: }, + { + id: 'liquidations', + title: t('statsPage.tabs.liquidations'), + content: , + }, + { id: 'rates', title: t('statsPage.tabs.rates'), content: }, ]} /> diff --git a/apps/evm/src/utilities/convertDecimalToPercentage/__tests__/index.spec.ts b/apps/evm/src/utilities/convertDecimalToPercentage/__tests__/index.spec.ts new file mode 100644 index 0000000000..056fd602c8 --- /dev/null +++ b/apps/evm/src/utilities/convertDecimalToPercentage/__tests__/index.spec.ts @@ -0,0 +1,19 @@ +import { convertDecimalToPercentage } from '..'; + +describe('convertDecimalToPercentage', () => { + it('converts a decimal fraction to its percentage representation', () => { + expect(convertDecimalToPercentage(0.05)).toBe(5); + }); + + it('handles zero', () => { + expect(convertDecimalToPercentage(0)).toBe(0); + }); + + it('handles values above one', () => { + expect(convertDecimalToPercentage(1.5)).toBe(150); + }); + + it('handles negative fractions', () => { + expect(convertDecimalToPercentage(-0.025)).toBe(-2.5); + }); +}); diff --git a/apps/evm/src/utilities/convertDecimalToPercentage/index.ts b/apps/evm/src/utilities/convertDecimalToPercentage/index.ts new file mode 100644 index 0000000000..0681ebb6de --- /dev/null +++ b/apps/evm/src/utilities/convertDecimalToPercentage/index.ts @@ -0,0 +1,3 @@ +// Converts a decimal fraction (e.g. 0.05) into its percentage representation +// (e.g. 5), ready to be passed to formatPercentageToReadableValue. +export const convertDecimalToPercentage = (decimal: number) => decimal * 100; diff --git a/apps/evm/src/utilities/index.ts b/apps/evm/src/utilities/index.ts index 1e44181c5e..d47e8034c0 100755 --- a/apps/evm/src/utilities/index.ts +++ b/apps/evm/src/utilities/index.ts @@ -24,6 +24,7 @@ export { default as convertPriceMantissaToDollars } from './convertPriceMantissa export { default as convertFactorFromSmartContract } from './convertFactorFromSmartContract'; export { default as findTokenByAddress } from './findTokenByAddress'; export * from './convertAprBipsToApy'; +export * from './convertDecimalToPercentage'; export * from './convertPercentageToBps'; export { default as extractSettledPromiseValue } from './extractSettledPromiseValue'; export { default as getUniqueTokenBalances } from './getUniqueTokenBalances';