From 5fa30e3b5c87abb4297a4e423bbc1c1e86fb0363 Mon Sep 17 00:00:00 2001 From: Gleiser Oliveira Date: Tue, 26 May 2026 19:18:07 -0600 Subject: [PATCH 1/9] chore: add new function keys --- apps/evm/src/constants/functionKey.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/evm/src/constants/functionKey.ts b/apps/evm/src/constants/functionKey.ts index 02db725581..606ceec3f4 100644 --- a/apps/evm/src/constants/functionKey.ts +++ b/apps/evm/src/constants/functionKey.ts @@ -80,6 +80,12 @@ enum FunctionKey { GET_RAW_TRADE_POSITIONS = 'GET_RAW_TRADE_POSITIONS', GET_TOKEN_PAIR_K_LINE_CANDLES = 'GET_TOKEN_PAIR_K_LINE_CANDLES', GET_TRADE_REDUCE_SWAP_QUOTES = 'GET_TRADE_REDUCE_SWAP_QUOTES', + GET_RISK_DASHBOARD_MARKET_AGGREGATES = 'GET_RISK_DASHBOARD_MARKET_AGGREGATES', + GET_RISK_DASHBOARD_MARKET_SNAPSHOTS = 'GET_RISK_DASHBOARD_MARKET_SNAPSHOTS', + GET_RISK_DASHBOARD_WALLET_AGGREGATES = 'GET_RISK_DASHBOARD_WALLET_AGGREGATES', + GET_RISK_DASHBOARD_TOP_WALLETS = 'GET_RISK_DASHBOARD_TOP_WALLETS', + GET_RISK_DASHBOARD_TRANSACTIONS_VOLUME = 'GET_RISK_DASHBOARD_TRANSACTIONS_VOLUME', + GET_IMAGE_ACCENT_COLOR = 'GET_IMAGE_ACCENT_COLOR', } export default FunctionKey; From 700f0707b10cb26aab3b3720a1ee8a843c8101c2 Mon Sep 17 00:00:00 2001 From: Gleiser Oliveira Date: Tue, 26 May 2026 19:18:24 -0600 Subject: [PATCH 2/9] feat: add getRiskDashboardWalletAggregates --- .../getRiskDashboardWalletAggregates/index.ts | 53 +++++++++++++++++++ .../useGetRiskDashboardWalletAggregates.ts | 29 ++++++++++ 2 files changed, 82 insertions(+) create mode 100644 apps/evm/src/clients/api/queries/getRiskDashboardWalletAggregates/index.ts create mode 100644 apps/evm/src/clients/api/queries/getRiskDashboardWalletAggregates/useGetRiskDashboardWalletAggregates.ts diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardWalletAggregates/index.ts b/apps/evm/src/clients/api/queries/getRiskDashboardWalletAggregates/index.ts new file mode 100644 index 0000000000..7206ba8de4 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardWalletAggregates/index.ts @@ -0,0 +1,53 @@ +import { VError } from 'libs/errors'; +import type { ChainId } from 'types'; +import { restService } from 'utilities'; +import type { ApiRiskDashboardAsOf } from '../getRiskDashboardMarketAggregates'; + +export interface GetRiskDashboardWalletAggregatesInput { + chainId: ChainId; +} + +export interface GetRiskDashboardWalletAggregatesResponse { + chainId: string; + asOf: ApiRiskDashboardAsOf; + totalSuppliers: string; + suppliersWithPositionAboveMinUsdValue: string; + totalBorrowers: string; + borrowersWithPositionAboveMinUsdValue: string; + walletsAtRisk: string; + walletsEligibleForLiquidation: string; + valueEligibleForLiquidationUsdCents: string; + totalCollateralAtRiskUsdCents: string; + totalBadDebtUsdCents: string; +} + +const HEALTH_FACTOR_AT_RISK_DECIMAL = 1.125; + +export async function getRiskDashboardWalletAggregates({ + chainId, +}: GetRiskDashboardWalletAggregatesInput) { + const response = await restService({ + endpoint: '/risk-dashboard/wallet-aggregates', + method: 'GET', + params: { + chainId, + healthFactorAtRiskDecimal: HEALTH_FACTOR_AT_RISK_DECIMAL, + }, + }); + + 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/getRiskDashboardWalletAggregates/useGetRiskDashboardWalletAggregates.ts b/apps/evm/src/clients/api/queries/getRiskDashboardWalletAggregates/useGetRiskDashboardWalletAggregates.ts new file mode 100644 index 0000000000..1679bdd967 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardWalletAggregates/useGetRiskDashboardWalletAggregates.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 GetRiskDashboardWalletAggregatesResponse, getRiskDashboardWalletAggregates } from '.'; + +export type UseGetRiskDashboardWalletAggregatesQueryKey = [ + FunctionKey.GET_RISK_DASHBOARD_WALLET_AGGREGATES, + { chainId: ChainId }, +]; + +type Options = QueryObserverOptions< + GetRiskDashboardWalletAggregatesResponse, + Error, + GetRiskDashboardWalletAggregatesResponse, + GetRiskDashboardWalletAggregatesResponse, + UseGetRiskDashboardWalletAggregatesQueryKey +>; + +export const useGetRiskDashboardWalletAggregates = (options?: Partial) => { + const { chainId } = useChainId(); + + return useQuery({ + queryKey: [FunctionKey.GET_RISK_DASHBOARD_WALLET_AGGREGATES, { chainId }], + queryFn: () => getRiskDashboardWalletAggregates({ chainId }), + ...options, + }); +}; From 1b95030ceb139bd5adc8818480e985e19c94f4c7 Mon Sep 17 00:00:00 2001 From: Gleiser Oliveira Date: Tue, 26 May 2026 19:21:15 -0600 Subject: [PATCH 3/9] feat: add getRiskDashboardTransactionsVolume --- .../index.ts | 63 +++++++++++++++++++ .../useGetRiskDashboardTransactionsVolume.ts | 39 ++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 apps/evm/src/clients/api/queries/getRiskDashboardTransactionsVolume/index.ts create mode 100644 apps/evm/src/clients/api/queries/getRiskDashboardTransactionsVolume/useGetRiskDashboardTransactionsVolume.ts diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardTransactionsVolume/index.ts b/apps/evm/src/clients/api/queries/getRiskDashboardTransactionsVolume/index.ts new file mode 100644 index 0000000000..bd4895b6d2 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardTransactionsVolume/index.ts @@ -0,0 +1,63 @@ +import { VError } from 'libs/errors'; +import type { ChainId } from 'types'; +import { restService } from 'utilities'; + +export interface ApiRiskDashboardTransactionsVolumeMarketBreakdown { + marketAddress: string; + mintUsdCents: string; + redeemUsdCents: string; + borrowUsdCents: string; + repayUsdCents: string; + liquidateUsdCents: string; + totalUsdCents: string; +} + +export interface ApiRiskDashboardTransactionsVolumeDay { + day: string; + mintUsdCents: string; + redeemUsdCents: string; + borrowUsdCents: string; + repayUsdCents: string; + liquidateUsdCents: string; + totalUsdCents: string; + byMarket: ApiRiskDashboardTransactionsVolumeMarketBreakdown[]; +} + +export interface GetRiskDashboardTransactionsVolumeInput { + chainId: ChainId; + days?: number; +} + +export interface GetRiskDashboardTransactionsVolumeResponse { + chainId: string; + days: number; + totalUsdCents: string; + series: ApiRiskDashboardTransactionsVolumeDay[]; +} + +export async function getRiskDashboardTransactionsVolume({ + chainId, + days, +}: GetRiskDashboardTransactionsVolumeInput) { + const response = await restService({ + endpoint: '/risk-dashboard/transactions-volume', + method: 'GET', + params: { chainId, days }, + }); + + 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/getRiskDashboardTransactionsVolume/useGetRiskDashboardTransactionsVolume.ts b/apps/evm/src/clients/api/queries/getRiskDashboardTransactionsVolume/useGetRiskDashboardTransactionsVolume.ts new file mode 100644 index 0000000000..5b3cf2b822 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardTransactionsVolume/useGetRiskDashboardTransactionsVolume.ts @@ -0,0 +1,39 @@ +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 GetRiskDashboardTransactionsVolumeResponse, + getRiskDashboardTransactionsVolume, +} from '.'; + +export interface UseGetRiskDashboardTransactionsVolumeInput { + days?: number; +} + +export type UseGetRiskDashboardTransactionsVolumeQueryKey = [ + FunctionKey.GET_RISK_DASHBOARD_TRANSACTIONS_VOLUME, + { chainId: ChainId; days?: number }, +]; + +type Options = QueryObserverOptions< + GetRiskDashboardTransactionsVolumeResponse, + Error, + GetRiskDashboardTransactionsVolumeResponse, + GetRiskDashboardTransactionsVolumeResponse, + UseGetRiskDashboardTransactionsVolumeQueryKey +>; + +export const useGetRiskDashboardTransactionsVolume = ( + { days }: UseGetRiskDashboardTransactionsVolumeInput = {}, + options?: Partial, +) => { + const { chainId } = useChainId(); + + return useQuery({ + queryKey: [FunctionKey.GET_RISK_DASHBOARD_TRANSACTIONS_VOLUME, { chainId, days }], + queryFn: () => getRiskDashboardTransactionsVolume({ chainId, days }), + ...options, + }); +}; From 8954d5f2799a0123d22d445f374268a2f3f0c133 Mon Sep 17 00:00:00 2001 From: Gleiser Oliveira Date: Tue, 26 May 2026 19:21:23 -0600 Subject: [PATCH 4/9] feat: add getRiskDashboardTopWallets --- .../getRiskDashboardTopWallets/index.ts | 62 +++++++++++++++++++ .../useGetRiskDashboardTopWallets.ts | 41 ++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 apps/evm/src/clients/api/queries/getRiskDashboardTopWallets/index.ts create mode 100644 apps/evm/src/clients/api/queries/getRiskDashboardTopWallets/useGetRiskDashboardTopWallets.ts diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardTopWallets/index.ts b/apps/evm/src/clients/api/queries/getRiskDashboardTopWallets/index.ts new file mode 100644 index 0000000000..3040ff0a0c --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardTopWallets/index.ts @@ -0,0 +1,62 @@ +import { VError } from 'libs/errors'; +import type { ChainId } from 'types'; +import { restService } from 'utilities'; +import type { ApiRiskDashboardAsOf } from '../getRiskDashboardMarketAggregates'; + +export type RiskDashboardTopWalletsKind = 'suppliers' | 'borrowers'; + +export interface ApiRiskDashboardTopWalletPosition { + marketAddress: string; + supplyUsdCents: string; + borrowUsdCents: string; + isCollateral: boolean; +} + +export interface ApiRiskDashboardTopWallet { + address: string; + totalSupplyUsdCents: string; + totalBorrowUsdCents: string; + healthFactorMantissa: string; + positions: ApiRiskDashboardTopWalletPosition[]; +} + +export interface GetRiskDashboardTopWalletsInput { + chainId: ChainId; + kind: RiskDashboardTopWalletsKind; + limit?: number; +} + +export interface GetRiskDashboardTopWalletsResponse { + chainId: string; + kind: RiskDashboardTopWalletsKind; + asOf: ApiRiskDashboardAsOf | null; + wallets: ApiRiskDashboardTopWallet[]; +} + +export async function getRiskDashboardTopWallets({ + chainId, + kind, + limit, +}: GetRiskDashboardTopWalletsInput) { + const response = await restService({ + endpoint: '/risk-dashboard/top-wallets', + method: 'GET', + params: { chainId, kind, limit }, + }); + + 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/getRiskDashboardTopWallets/useGetRiskDashboardTopWallets.ts b/apps/evm/src/clients/api/queries/getRiskDashboardTopWallets/useGetRiskDashboardTopWallets.ts new file mode 100644 index 0000000000..a7665f2e9b --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardTopWallets/useGetRiskDashboardTopWallets.ts @@ -0,0 +1,41 @@ +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 GetRiskDashboardTopWalletsResponse, + type RiskDashboardTopWalletsKind, + getRiskDashboardTopWallets, +} from '.'; + +export interface UseGetRiskDashboardTopWalletsInput { + kind: RiskDashboardTopWalletsKind; + limit?: number; +} + +export type UseGetRiskDashboardTopWalletsQueryKey = [ + FunctionKey.GET_RISK_DASHBOARD_TOP_WALLETS, + { chainId: ChainId; kind: RiskDashboardTopWalletsKind; limit?: number }, +]; + +type Options = QueryObserverOptions< + GetRiskDashboardTopWalletsResponse, + Error, + GetRiskDashboardTopWalletsResponse, + GetRiskDashboardTopWalletsResponse, + UseGetRiskDashboardTopWalletsQueryKey +>; + +export const useGetRiskDashboardTopWallets = ( + { kind, limit }: UseGetRiskDashboardTopWalletsInput, + options?: Partial, +) => { + const { chainId } = useChainId(); + + return useQuery({ + queryKey: [FunctionKey.GET_RISK_DASHBOARD_TOP_WALLETS, { chainId, kind, limit }], + queryFn: () => getRiskDashboardTopWallets({ chainId, kind, limit }), + ...options, + }); +}; From 85f785de6508c604a4c09b8f98c553c651f6d6a9 Mon Sep 17 00:00:00 2001 From: Gleiser Oliveira Date: Tue, 26 May 2026 19:21:32 -0600 Subject: [PATCH 5/9] feat: add getRiskDashboardMarketSnapshots --- .../getRiskDashboardMarketSnapshots/index.ts | 57 +++++++++++++++++++ .../useGetRiskDashboardMarketSnapshots.ts | 36 ++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 apps/evm/src/clients/api/queries/getRiskDashboardMarketSnapshots/index.ts create mode 100644 apps/evm/src/clients/api/queries/getRiskDashboardMarketSnapshots/useGetRiskDashboardMarketSnapshots.ts diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardMarketSnapshots/index.ts b/apps/evm/src/clients/api/queries/getRiskDashboardMarketSnapshots/index.ts new file mode 100644 index 0000000000..2df605e1db --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardMarketSnapshots/index.ts @@ -0,0 +1,57 @@ +import { VError } from 'libs/errors'; +import type { ChainId } from 'types'; +import { restService } from 'utilities'; + +export type RiskDashboardMarketSnapshotKind = 'day' | 'week' | 'month' | 'year'; + +export interface ApiRiskDashboardMarketSnapshotPoint { + marketAddress: string; + supplyUsdCents: string; + borrowsUsdCents: string; +} + +export interface ApiRiskDashboardMarketSnapshotSlot { + blockNumber: string; + blockTimestamp: string; + totalSupplyUsdCents: string; + totalBorrowsUsdCents: string; + byMarket: ApiRiskDashboardMarketSnapshotPoint[]; +} + +export interface GetRiskDashboardMarketSnapshotsInput { + chainId: ChainId; + kind: RiskDashboardMarketSnapshotKind; +} + +export interface GetRiskDashboardMarketSnapshotsResponse { + chainId: string; + kind: RiskDashboardMarketSnapshotKind; + series: ApiRiskDashboardMarketSnapshotSlot[]; +} + +export async function getRiskDashboardMarketSnapshots({ + chainId, + kind, +}: GetRiskDashboardMarketSnapshotsInput) { + const response = await restService({ + endpoint: '/risk-dashboard/market-snapshots', + method: 'GET', + params: { chainId, kind }, + }); + + 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/getRiskDashboardMarketSnapshots/useGetRiskDashboardMarketSnapshots.ts b/apps/evm/src/clients/api/queries/getRiskDashboardMarketSnapshots/useGetRiskDashboardMarketSnapshots.ts new file mode 100644 index 0000000000..a8885d3534 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardMarketSnapshots/useGetRiskDashboardMarketSnapshots.ts @@ -0,0 +1,36 @@ +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 GetRiskDashboardMarketSnapshotsResponse, + type RiskDashboardMarketSnapshotKind, + getRiskDashboardMarketSnapshots, +} from '.'; + +export type UseGetRiskDashboardMarketSnapshotsQueryKey = [ + FunctionKey.GET_RISK_DASHBOARD_MARKET_SNAPSHOTS, + { chainId: ChainId; kind: RiskDashboardMarketSnapshotKind }, +]; + +type Options = QueryObserverOptions< + GetRiskDashboardMarketSnapshotsResponse, + Error, + GetRiskDashboardMarketSnapshotsResponse, + GetRiskDashboardMarketSnapshotsResponse, + UseGetRiskDashboardMarketSnapshotsQueryKey +>; + +export const useGetRiskDashboardMarketSnapshots = ( + { kind }: { kind: RiskDashboardMarketSnapshotKind }, + options?: Partial, +) => { + const { chainId } = useChainId(); + + return useQuery({ + queryKey: [FunctionKey.GET_RISK_DASHBOARD_MARKET_SNAPSHOTS, { chainId, kind }], + queryFn: () => getRiskDashboardMarketSnapshots({ chainId, kind }), + ...options, + }); +}; From 8edbaeae4d509a7a2b68e3863388f27083dd8819 Mon Sep 17 00:00:00 2001 From: Gleiser Oliveira Date: Tue, 26 May 2026 19:21:41 -0600 Subject: [PATCH 6/9] feat: add getRiskDashboardMarketAggregates --- .../getRiskDashboardMarketAggregates/index.ts | 47 +++++++++++++++++++ .../useGetRiskDashboardMarketAggregates.ts | 29 ++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 apps/evm/src/clients/api/queries/getRiskDashboardMarketAggregates/index.ts create mode 100644 apps/evm/src/clients/api/queries/getRiskDashboardMarketAggregates/useGetRiskDashboardMarketAggregates.ts diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardMarketAggregates/index.ts b/apps/evm/src/clients/api/queries/getRiskDashboardMarketAggregates/index.ts new file mode 100644 index 0000000000..85d85f74e1 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardMarketAggregates/index.ts @@ -0,0 +1,47 @@ +import { VError } from 'libs/errors'; +import type { ChainId } from 'types'; +import { restService } from 'utilities'; + +export interface ApiRiskDashboardAsOf { + blockNumber: string; + blockTimestamp: string; +} + +export interface GetRiskDashboardMarketAggregatesInput { + chainId: ChainId; +} + +export interface GetRiskDashboardMarketAggregatesResponse { + chainId: string; + asOf: ApiRiskDashboardAsOf; + totalSupplyUsdCents: string; + totalBorrowsUsdCents: string; + liquidityUsdCents: string; + utilization: number; +} + +export async function getRiskDashboardMarketAggregates({ + chainId, +}: GetRiskDashboardMarketAggregatesInput) { + const response = await restService({ + endpoint: '/risk-dashboard/market-aggregates', + 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/getRiskDashboardMarketAggregates/useGetRiskDashboardMarketAggregates.ts b/apps/evm/src/clients/api/queries/getRiskDashboardMarketAggregates/useGetRiskDashboardMarketAggregates.ts new file mode 100644 index 0000000000..092cba9c81 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardMarketAggregates/useGetRiskDashboardMarketAggregates.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 GetRiskDashboardMarketAggregatesResponse, getRiskDashboardMarketAggregates } from '.'; + +export type UseGetRiskDashboardMarketAggregatesQueryKey = [ + FunctionKey.GET_RISK_DASHBOARD_MARKET_AGGREGATES, + { chainId: ChainId }, +]; + +type Options = QueryObserverOptions< + GetRiskDashboardMarketAggregatesResponse, + Error, + GetRiskDashboardMarketAggregatesResponse, + GetRiskDashboardMarketAggregatesResponse, + UseGetRiskDashboardMarketAggregatesQueryKey +>; + +export const useGetRiskDashboardMarketAggregates = (options?: Partial) => { + const { chainId } = useChainId(); + + return useQuery({ + queryKey: [FunctionKey.GET_RISK_DASHBOARD_MARKET_AGGREGATES, { chainId }], + queryFn: () => getRiskDashboardMarketAggregates({ chainId }), + ...options, + }); +}; From 8711f64236c13e288358cc65d1b5a2fe64394ac1 Mon Sep 17 00:00:00 2001 From: Gleiser Oliveira Date: Tue, 26 May 2026 19:21:53 -0600 Subject: [PATCH 7/9] feat: add useImageAccentColors --- .../src/hooks/useImageAccentColors/index.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 apps/evm/src/hooks/useImageAccentColors/index.ts diff --git a/apps/evm/src/hooks/useImageAccentColors/index.ts b/apps/evm/src/hooks/useImageAccentColors/index.ts new file mode 100644 index 0000000000..2afc8d73ce --- /dev/null +++ b/apps/evm/src/hooks/useImageAccentColors/index.ts @@ -0,0 +1,32 @@ +import { getPalette } from '@lauriys/react-palette'; +import { useQueries } from '@tanstack/react-query'; +import FunctionKey from 'constants/functionKey'; + +const fetchVibrantColor = async (imagePath: string) => { + const palette = await getPalette(imagePath); + if (!palette.vibrant) { + throw new Error(`No vibrant color extracted from ${imagePath}`); + } + return palette.vibrant; +}; + +export const useImageAccentColors = (imagePaths: string[]) => { + const uniquePaths = Array.from(new Set(imagePaths.filter(Boolean))); + + const results = useQueries({ + queries: uniquePaths.map(imagePath => ({ + queryKey: [FunctionKey.GET_IMAGE_ACCENT_COLOR, imagePath], + queryFn: () => fetchVibrantColor(imagePath), + staleTime: Number.POSITIVE_INFINITY, + })), + }); + + const colors: Record = {}; + uniquePaths.forEach((path, index) => { + const color = results[index].data; + if (color) { + colors[path] = color; + } + }); + return colors; +}; From fe2b5c2cff95844a3d556b7163ccdf29b7b96fa1 Mon Sep 17 00:00:00 2001 From: Gleiser Oliveira Date: Tue, 26 May 2026 19:23:58 -0600 Subject: [PATCH 8/9] feat: add Stats - Overview --- apps/evm/src/clients/api/index.ts | 11 + apps/evm/src/libs/errors/handleError/index.ts | 2 +- .../libs/translations/translations/en.json | 84 +++- .../Stats/HistoricalDominanceChart/index.tsx | 167 ++++++++ .../Stats/HistoricalLiquidityChart/index.tsx | 157 ++++++++ .../Stats/HistoricalMarketChart/index.tsx | 309 ++++++++++++++ apps/evm/src/pages/Stats/MarketKpis/index.tsx | 52 +++ .../evm/src/pages/Stats/StatsIframe/index.tsx | 47 --- .../TopWallets/TopWalletsTable/index.tsx | 377 ++++++++++++++++++ apps/evm/src/pages/Stats/TopWallets/index.tsx | 21 + .../pages/Stats/TransactionsVolume/index.tsx | 226 +++++++++++ apps/evm/src/pages/Stats/WalletKpis/index.tsx | 74 ++++ apps/evm/src/pages/Stats/index.tsx | 53 ++- apps/evm/src/pages/Stats/roundUpToScale.ts | 8 + apps/evm/src/pages/Stats/useMarketColors.ts | 227 +++++++++++ 15 files changed, 1758 insertions(+), 57 deletions(-) create mode 100644 apps/evm/src/pages/Stats/HistoricalDominanceChart/index.tsx create mode 100644 apps/evm/src/pages/Stats/HistoricalLiquidityChart/index.tsx create mode 100644 apps/evm/src/pages/Stats/HistoricalMarketChart/index.tsx create mode 100644 apps/evm/src/pages/Stats/MarketKpis/index.tsx delete mode 100644 apps/evm/src/pages/Stats/StatsIframe/index.tsx create mode 100644 apps/evm/src/pages/Stats/TopWallets/TopWalletsTable/index.tsx create mode 100644 apps/evm/src/pages/Stats/TopWallets/index.tsx create mode 100644 apps/evm/src/pages/Stats/TransactionsVolume/index.tsx create mode 100644 apps/evm/src/pages/Stats/WalletKpis/index.tsx create mode 100644 apps/evm/src/pages/Stats/roundUpToScale.ts create mode 100644 apps/evm/src/pages/Stats/useMarketColors.ts diff --git a/apps/evm/src/clients/api/index.ts b/apps/evm/src/clients/api/index.ts index 41a1dc6470..e03cea4a83 100644 --- a/apps/evm/src/clients/api/index.ts +++ b/apps/evm/src/clients/api/index.ts @@ -267,3 +267,14 @@ export * from './queries/getRawTradePositions/useGetRawTradePositions'; export * from './queries/getTradeReduceSwapQuotes'; export * from './queries/getTradeReduceSwapQuotes/useGetTradeReduceSwapQuotes'; + +export * from './queries/getRiskDashboardMarketAggregates'; +export * from './queries/getRiskDashboardMarketAggregates/useGetRiskDashboardMarketAggregates'; +export * from './queries/getRiskDashboardMarketSnapshots'; +export * from './queries/getRiskDashboardMarketSnapshots/useGetRiskDashboardMarketSnapshots'; +export * from './queries/getRiskDashboardWalletAggregates'; +export * from './queries/getRiskDashboardWalletAggregates/useGetRiskDashboardWalletAggregates'; +export * from './queries/getRiskDashboardTopWallets'; +export * from './queries/getRiskDashboardTopWallets/useGetRiskDashboardTopWallets'; +export * from './queries/getRiskDashboardTransactionsVolume'; +export * from './queries/getRiskDashboardTransactionsVolume/useGetRiskDashboardTransactionsVolume'; diff --git a/apps/evm/src/libs/errors/handleError/index.ts b/apps/evm/src/libs/errors/handleError/index.ts index b83c4249f5..d8af3db4bc 100644 --- a/apps/evm/src/libs/errors/handleError/index.ts +++ b/apps/evm/src/libs/errors/handleError/index.ts @@ -1,4 +1,4 @@ -import { BaseError } from 'viem'; +import type { BaseError } from 'viem'; import { displayNotification } from 'libs/notifications'; diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index 010829403c..22e5999eb0 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1583,9 +1583,91 @@ }, "statsPage": { "illustrationAlt": "Stats illustration", + "marketKpis": { + "liquidity": "Liquidity", + "totalBorrows": "Total Borrows", + "totalSupply": "Total Supply", + "unavailable": "Market data unavailable.", + "utilization": "Utilization" + }, + "historicalDominance": { + "borrows": { + "noData": "No historical snapshots yet.", + "ratioLabel": "Borrows / Liquidity", + "title": "Borrow Dominance over Time", + "unavailable": "Borrow dominance unavailable." + }, + "supply": { + "noData": "No historical snapshots yet.", + "ratioLabel": "Supply / Liquidity", + "title": "Supply Dominance over Time", + "unavailable": "Supply dominance unavailable." + } + }, + "historicalLiquidity": { + "noData": "No historical snapshots yet.", + "title": "Liquidity over Time", + "tooltipLabel": "Liquidity", + "unavailable": "Historical liquidity unavailable." + }, + "historicalMarketChart": { + "borrows": { + "noData": "No historical snapshots yet.", + "title": "Borrows by Assets over Time", + "unavailable": "Historical borrows unavailable." + }, + "others": "Others", + "supply": { + "noData": "No historical snapshots yet.", + "title": "Deposits by Assets over Time", + "unavailable": "Historical supply unavailable." + } + }, "poweredBy": "Powered by {{provider}}", "subtitle": "Real-time risk parameters for Venus Protocol markets", - "title": "Venus Stats" + "title": "Venus Stats", + "topWallets": { + "noData": "No data yet.", + "topBorrowers": "Top Borrowers", + "topSuppliers": "Top Suppliers", + "tooltip": { + "borrow": "Borrow", + "supply": "Supply" + }, + "unavailableBorrowers": "Top borrowers unavailable.", + "unavailableSuppliers": "Top suppliers unavailable." + }, + "transactionsVolume": { + "legend": { + "borrow": "Borrow", + "liquidate": "Liquidate", + "mint": "Mint", + "redeem": "Redeem", + "repay": "Repay" + }, + "subtitle": "Last {{days}} days ยท {{total}}", + "title": "Transactions Volume", + "tooltipTotal": "Total", + "unavailable": "Transactions volume unavailable." + }, + "tabs": { + "liquidations": "Liquidations", + "markets": "Markets", + "overview": "Overview", + "wallets": "Wallets" + }, + "walletKpis": { + "borrowersWithPositionAboveMinUsdValue": "Borrowers > $10", + "eligibleForLiquidation": "Wallets Eligible for Liquidation", + "suppliersWithPositionAboveMinUsdValue": "Suppliers > $10", + "totalBadDebt": "Total Bad Debt", + "totalBorrowers": "Total Borrowers", + "totalCollateralAtRisk": "Total Collateral at Risk", + "totalSuppliers": "Total Suppliers", + "unavailable": "Wallet data unavailable.", + "valueEligibleForLiquidation": "Value Eligible for Liquidation", + "walletsAtRisk": "Wallets at Risk" + } }, "swap": { "errors": { diff --git a/apps/evm/src/pages/Stats/HistoricalDominanceChart/index.tsx b/apps/evm/src/pages/Stats/HistoricalDominanceChart/index.tsx new file mode 100644 index 0000000000..b1fafefd84 --- /dev/null +++ b/apps/evm/src/pages/Stats/HistoricalDominanceChart/index.tsx @@ -0,0 +1,167 @@ +import { theme } from '@venusprotocol/ui'; +import { useGetRiskDashboardMarketSnapshots } 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'; + +type Metric = 'supply' | 'borrows'; + +interface ChartDatum { + timestamp: number; + blockNumber: string; + ratio: number; +} + +const toDollars = (cents: string) => Number(cents) / 100; + +const formatRatioPercent = (ratio: number) => `${(ratio * 100).toFixed(0)}%`; + +const formatDateLabel = (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); +}; + +const formatTooltipDate = (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +}; + +interface TooltipPayloadItem { + value: number; + payload: ChartDatum; +} + +const DominanceTooltip: React.FC<{ + active?: boolean; + payload?: TooltipPayloadItem[]; + ratioLabel: string; +}> = ({ active, payload, ratioLabel }) => { + if (!active || !payload || payload.length === 0) { + return null; + } + const datum = payload[0].payload; + + return ( +
+
+ {formatTooltipDate(datum.timestamp)} +
+
+ {ratioLabel} + {formatRatioPercent(datum.ratio)} +
+
+ ); +}; + +export interface HistoricalDominanceChartProps { + metric: Metric; +} + +export const HistoricalDominanceChart: React.FC = ({ metric }) => { + const { t } = useTranslation(); + const { data, isLoading, isError } = useGetRiskDashboardMarketSnapshots({ kind: 'year' }); + + const chartData = useMemo(() => { + if (!data) { + return []; + } + const points: ChartDatum[] = []; + for (const slot of data.series) { + const supply = toDollars(slot.totalSupplyUsdCents); + const borrows = toDollars(slot.totalBorrowsUsdCents); + const liquidity = supply - borrows; + if (liquidity <= 0) { + continue; + } + const numerator = metric === 'supply' ? supply : borrows; + points.push({ + timestamp: new Date(slot.blockTimestamp).getTime(), + blockNumber: slot.blockNumber, + ratio: numerator / liquidity, + }); + } + return points; + }, [data, metric]); + + const title = t(`statsPage.historicalDominance.${metric}.title`); + const unavailableMessage = t(`statsPage.historicalDominance.${metric}.unavailable`); + const noDataMessage = t(`statsPage.historicalDominance.${metric}.noData`); + const ratioLabel = t(`statsPage.historicalDominance.${metric}.ratioLabel`); + const lineColor = metric === 'supply' ? theme.colors.blue : theme.colors.red; + + return ( + +

{title}

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

{unavailableMessage}

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

{noDataMessage}

+ ) : ( + + + + + + + ( + + {formatRatioPercent(payload.value)} + + )} + /> + + } + /> + + + + + )} +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/HistoricalLiquidityChart/index.tsx b/apps/evm/src/pages/Stats/HistoricalLiquidityChart/index.tsx new file mode 100644 index 0000000000..2f1fee43b6 --- /dev/null +++ b/apps/evm/src/pages/Stats/HistoricalLiquidityChart/index.tsx @@ -0,0 +1,157 @@ +import { theme } from '@venusprotocol/ui'; +import { useGetRiskDashboardMarketSnapshots } from 'clients/api'; +import { Card, Spinner } from 'components'; +import { useTranslation } from 'libs/translations'; +import { useMemo } from 'react'; +import { + Area, + CartesianGrid, + AreaChart as RCAreaChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { formatCentsToReadableValue } from 'utilities'; +import { roundUpToScale } from '../roundUpToScale'; + +interface ChartDatum { + timestamp: number; + blockNumber: string; + liquidity: number; +} + +const toDollars = (cents: string) => Number(cents) / 100; + +const formatDollarsToCents = (dollars: number) => + formatCentsToReadableValue({ value: Math.round(Math.abs(dollars) * 100) }); + +const formatDateLabel = (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); +}; + +const formatTooltipDate = (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +}; + +interface TooltipPayloadItem { + value: number; + payload: ChartDatum; +} + +const LiquidityTooltip: React.FC<{ + active?: boolean; + payload?: TooltipPayloadItem[]; + liquidityLabel: string; +}> = ({ active, payload, liquidityLabel }) => { + if (!active || !payload || payload.length === 0) { + return null; + } + const datum = payload[0].payload; + + return ( +
+
+ {formatTooltipDate(datum.timestamp)} +
+
+ {liquidityLabel} + {formatDollarsToCents(datum.liquidity)} +
+
+ ); +}; + +export const HistoricalLiquidityChart: React.FC = () => { + const { t } = useTranslation(); + const { data, isLoading, isError } = useGetRiskDashboardMarketSnapshots({ kind: 'year' }); + + const chartData = useMemo(() => { + if (!data) { + return []; + } + return data.series.map(slot => { + const supply = toDollars(slot.totalSupplyUsdCents); + const borrows = toDollars(slot.totalBorrowsUsdCents); + return { + timestamp: new Date(slot.blockTimestamp).getTime(), + blockNumber: slot.blockNumber, + liquidity: supply - borrows, + }; + }); + }, [data]); + + return ( + +

{t('statsPage.historicalLiquidity.title')}

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

{t('statsPage.historicalLiquidity.unavailable')}

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

{t('statsPage.historicalLiquidity.noData')}

+ ) : ( + + + + + + + ( + + {formatDollarsToCents(payload.value)} + + )} + /> + + + } + /> + + + + + )} +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/HistoricalMarketChart/index.tsx b/apps/evm/src/pages/Stats/HistoricalMarketChart/index.tsx new file mode 100644 index 0000000000..9c2823fc89 --- /dev/null +++ b/apps/evm/src/pages/Stats/HistoricalMarketChart/index.tsx @@ -0,0 +1,309 @@ +import { theme } from '@venusprotocol/ui'; +import { + type ApiRiskDashboardMarketSnapshotSlot, + useGetRiskDashboardMarketSnapshots, +} 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 { formatCentsToReadableValue } from 'utilities'; +import { getAddress } from 'viem'; +import { roundUpToScale } from '../roundUpToScale'; +import { useMarketColors } from '../useMarketColors'; + +const OTHERS_KEY = 'others'; +const OTHERS_THRESHOLD_DOLLARS = 50_000_000; + +type Metric = 'supply' | 'borrows'; + +interface MarketMeta { + symbol: string; +} + +interface ChartDatum { + timestamp: number; + blockNumber: string; + total: number; + [marketAddressOrOthers: string]: number | string; +} + +const toDollars = (cents: string) => Number(cents) / 100; + +const formatDollarsToCents = (dollars: number) => + formatCentsToReadableValue({ value: Math.round(Math.abs(dollars) * 100) }); + +const formatDateLabel = (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); +}; + +const formatTooltipDate = (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +}; + +const readPointValue = ( + point: { supplyUsdCents: string; borrowsUsdCents: string }, + metric: Metric, +) => (metric === 'supply' ? toDollars(point.supplyUsdCents) : toDollars(point.borrowsUsdCents)); + +const readSlotTotal = (slot: ApiRiskDashboardMarketSnapshotSlot, metric: Metric) => + metric === 'supply' ? toDollars(slot.totalSupplyUsdCents) : toDollars(slot.totalBorrowsUsdCents); + +const computePeakByMarket = (series: ApiRiskDashboardMarketSnapshotSlot[], metric: Metric) => { + const peaks = new Map(); + for (const slot of series) { + for (const point of slot.byMarket) { + const market = getAddress(point.marketAddress); + const value = readPointValue(point, metric); + const currentPeak = peaks.get(market) ?? 0; + if (value > currentPeak) { + peaks.set(market, value); + } + } + } + return peaks; +}; + +const buildChartData = ( + series: ApiRiskDashboardMarketSnapshotSlot[], + metric: Metric, + isMajorMarket: (market: string) => boolean, +) => + series.map(slot => { + const datum: ChartDatum = { + timestamp: new Date(slot.blockTimestamp).getTime(), + blockNumber: slot.blockNumber, + total: readSlotTotal(slot, metric), + }; + + let othersValue = 0; + for (const point of slot.byMarket) { + const market = getAddress(point.marketAddress); + const value = readPointValue(point, metric); + if (isMajorMarket(market)) { + datum[market] = value; + } else { + othersValue += value; + } + } + if (othersValue > 0) { + datum[OTHERS_KEY] = othersValue; + } + return datum; + }); + +const orderMajorMarketsByPeak = (peakByMarket: Map) => + [...peakByMarket.entries()] + .filter(([, peak]) => peak >= OTHERS_THRESHOLD_DOLLARS) + .sort((entryA, entryB) => entryB[1] - entryA[1]) + .map(([market]) => market); + +interface TooltipPayloadItem { + dataKey: unknown; + value: number; + color: string; + payload: ChartDatum; +} + +const HistoricalTooltip: React.FC<{ + active?: boolean; + payload?: TooltipPayloadItem[]; + marketMetaByAddress: Record; + othersLabel: string; +}> = ({ active, payload, marketMetaByAddress, othersLabel }) => { + 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; + } + if (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 ( +
+
+ {formatTooltipDate(datum.timestamp)} + {formatDollarsToCents(datum.total)} +
+ + {breakdown.map(item => { + const label = + item.key === OTHERS_KEY + ? othersLabel + : marketMetaByAddress[item.key]?.symbol ?? item.key.slice(0, 8); + return ( +
+
+ + {label} +
+ + {formatDollarsToCents(item.value)} + +
+ ); + })} +
+ ); +}; + +export interface HistoricalMarketChartProps { + metric: Metric; +} + +export const HistoricalMarketChart: React.FC = ({ metric }) => { + const { t } = useTranslation(); + const { data, isLoading, isError } = useGetRiskDashboardMarketSnapshots({ kind: 'year' }); + const vTokens = useGetVTokens(); + const { colorByMarket } = useMarketColors(); + + const marketMetaByAddress = useMemo(() => { + const map: Record = {}; + for (const vToken of vTokens) { + map[getAddress(vToken.address)] = { symbol: vToken.underlyingToken.symbol }; + } + return map; + }, [vTokens]); + + const peakByMarket = useMemo( + () => (data ? computePeakByMarket(data.series, metric) : new Map()), + [data, metric], + ); + + const majorMarkets = useMemo(() => orderMajorMarketsByPeak(peakByMarket), [peakByMarket]); + + const majorMarketSet = useMemo(() => new Set(majorMarkets), [majorMarkets]); + + const chartData = useMemo( + () => (data ? buildChartData(data.series, metric, market => majorMarketSet.has(market)) : []), + [data, metric, majorMarketSet], + ); + + const hasOthers = useMemo( + () => chartData.some(datum => typeof datum[OTHERS_KEY] === 'number'), + [chartData], + ); + + const title = t(`statsPage.historicalMarketChart.${metric}.title`); + const unavailableMessage = t(`statsPage.historicalMarketChart.${metric}.unavailable`); + const noDataMessage = t(`statsPage.historicalMarketChart.${metric}.noData`); + const othersLabel = t('statsPage.historicalMarketChart.others'); + + return ( + +

{title}

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

{unavailableMessage}

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

{noDataMessage}

+ ) : ( + + + + + + + ( + + {formatDollarsToCents(payload.value)} + + )} + /> + + + } + /> + + {majorMarkets.map(market => ( + + ))} + + {hasOthers && ( + + )} + + + )} +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/MarketKpis/index.tsx b/apps/evm/src/pages/Stats/MarketKpis/index.tsx new file mode 100644 index 0000000000..dab2c94f5b --- /dev/null +++ b/apps/evm/src/pages/Stats/MarketKpis/index.tsx @@ -0,0 +1,52 @@ +import { useGetRiskDashboardMarketAggregates } from 'clients/api'; +import { Card, Spinner } from 'components'; +import { useTranslation } from 'libs/translations'; +import { formatCentsToReadableValue, formatPercentageToReadableValue } from 'utilities'; + +interface KpiCellProps { + label: string; + value: string; +} + +const KpiCell: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +export const MarketKpis: React.FC = () => { + const { t } = useTranslation(); + const { data, isLoading, isError } = useGetRiskDashboardMarketAggregates(); + + if (isLoading) { + return ; + } + + if (isError || !data) { + return

{t('statsPage.marketKpis.unavailable')}

; + } + + return ( + +
+ + + + +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/StatsIframe/index.tsx b/apps/evm/src/pages/Stats/StatsIframe/index.tsx deleted file mode 100644 index faac917b5e..0000000000 --- a/apps/evm/src/pages/Stats/StatsIframe/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Spinner } from 'components'; -import { Link } from 'containers/Link'; -import { useTranslation } from 'libs/translations'; -import { useState } from 'react'; - -const STATS_IFRAME_URL = - 'https://app.hex.tech/10609151-106a-4740-8982-17a9a4e59699/app/Venus-032RSn52D8LeH6K6o73Edt/latest?embedded=true'; - -const ALLEZ_URL = 'https://allez.xyz/dashboards/venus/risk'; - -export const StatsIframe: React.FC = () => { - const { t, Trans } = useTranslation(); - const [isLoading, setIsLoading] = useState(true); - - return ( -
-
- {isLoading && } - -