diff --git a/apps/evm/src/clients/api/index.ts b/apps/evm/src/clients/api/index.ts index adffcb3766..ad8a8686d8 100644 --- a/apps/evm/src/clients/api/index.ts +++ b/apps/evm/src/clients/api/index.ts @@ -327,6 +327,12 @@ export * from './queries/getRiskDashboardMarkets'; export * from './queries/getRiskDashboardMarkets/useGetRiskDashboardMarkets'; export * from './queries/getRiskDashboardEModePools'; export * from './queries/getRiskDashboardEModePools/useGetRiskDashboardEModePools'; +export * from './queries/getRiskDashboardLiquidationsSummary'; +export * from './queries/getRiskDashboardLiquidationsSummary/useGetRiskDashboardLiquidationsSummary'; +export * from './queries/getRiskDashboardLiquidationsDaily'; +export * from './queries/getRiskDashboardLiquidationsDaily/useGetRiskDashboardLiquidationsDaily'; +export * from './queries/getRiskDashboardLiquidationsDistribution'; +export * from './queries/getRiskDashboardLiquidationsDistribution/useGetRiskDashboardLiquidationsDistribution'; export * from './queries/getRiskDashboardMarketSnapshots'; export * from './queries/getRiskDashboardMarketSnapshots/useGetRiskDashboardMarketSnapshots'; export * from './queries/getRiskDashboardWalletAggregates'; diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDaily/index.ts b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDaily/index.ts new file mode 100644 index 0000000000..86e6feb058 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDaily/index.ts @@ -0,0 +1,29 @@ +import type { ChainId } from 'types'; +import { fetchRiskDashboard } from '../fetchRiskDashboard'; + +export interface ApiRiskDashboardLiquidationsDailyDay { + day: string; + count: string; + debtRepaidUsdCents: string; + collateralSeizedUsdCents: string; + activeLiquidators: string; +} + +export interface GetRiskDashboardLiquidationsDailyResponse { + chainId: string; + days: number; + series: ApiRiskDashboardLiquidationsDailyDay[]; +} + +export const getRiskDashboardLiquidationsDaily = ({ + chainId, + days, +}: { + chainId: ChainId; + days: number; +}) => + fetchRiskDashboard({ + endpoint: '/risk-dashboard/liquidations-daily', + chainId, + params: { days }, + }); diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDaily/useGetRiskDashboardLiquidationsDaily.ts b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDaily/useGetRiskDashboardLiquidationsDaily.ts new file mode 100644 index 0000000000..92041d555a --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDaily/useGetRiskDashboardLiquidationsDaily.ts @@ -0,0 +1,38 @@ +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 GetRiskDashboardLiquidationsDailyResponse, + getRiskDashboardLiquidationsDaily, +} from '.'; + +export type UseGetRiskDashboardLiquidationsDailyQueryKey = [ + FunctionKey.GET_RISK_DASHBOARD_LIQUIDATIONS_DAILY, + { chainId: ChainId; days: number }, +]; + +type Options = QueryObserverOptions< + GetRiskDashboardLiquidationsDailyResponse, + Error, + GetRiskDashboardLiquidationsDailyResponse, + GetRiskDashboardLiquidationsDailyResponse, + UseGetRiskDashboardLiquidationsDailyQueryKey +>; + +export const useGetRiskDashboardLiquidationsDaily = ( + { days }: { days: number }, + options?: Partial, +) => { + const { chainId } = useChainId(); + + return useQuery({ + queryKey: [ + FunctionKey.GET_RISK_DASHBOARD_LIQUIDATIONS_DAILY, + { chainId, days }, + ], + queryFn: () => getRiskDashboardLiquidationsDaily({ chainId, days }), + ...options, + }); +}; diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDistribution/index.ts b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDistribution/index.ts new file mode 100644 index 0000000000..d69a07b5c0 --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDistribution/index.ts @@ -0,0 +1,33 @@ +import type { ChainId } from 'types'; +import type { Address } from 'viem'; +import { fetchRiskDashboard } from '../fetchRiskDashboard'; + +export type LiquidationsDistributionAxis = 'collateral' | 'debt'; + +export interface ApiRiskDashboardLiquidationsDistributionRow { + marketAddress: Address; + count: string; + valueUsdCents: string; +} + +export interface GetRiskDashboardLiquidationsDistributionResponse { + chainId: string; + days: number; + by: LiquidationsDistributionAxis; + rows: ApiRiskDashboardLiquidationsDistributionRow[]; +} + +export const getRiskDashboardLiquidationsDistribution = ({ + chainId, + days, + by, +}: { + chainId: ChainId; + days: number; + by: LiquidationsDistributionAxis; +}) => + fetchRiskDashboard({ + endpoint: '/risk-dashboard/liquidations-distribution', + chainId, + params: { days, by }, + }); diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDistribution/useGetRiskDashboardLiquidationsDistribution.ts b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDistribution/useGetRiskDashboardLiquidationsDistribution.ts new file mode 100644 index 0000000000..5ecca831cd --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsDistribution/useGetRiskDashboardLiquidationsDistribution.ts @@ -0,0 +1,40 @@ +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 GetRiskDashboardLiquidationsDistributionResponse, + type LiquidationsDistributionAxis, + getRiskDashboardLiquidationsDistribution, +} from '.'; + +export type UseGetRiskDashboardLiquidationsDistributionQueryKey = [ + FunctionKey.GET_RISK_DASHBOARD_LIQUIDATIONS_DISTRIBUTION, + { chainId: ChainId; days: number; by: LiquidationsDistributionAxis }, +]; + +type Options = QueryObserverOptions< + GetRiskDashboardLiquidationsDistributionResponse, + Error, + GetRiskDashboardLiquidationsDistributionResponse, + GetRiskDashboardLiquidationsDistributionResponse, + UseGetRiskDashboardLiquidationsDistributionQueryKey +>; + +export const useGetRiskDashboardLiquidationsDistribution = ( + { days, by }: { days: number; by: LiquidationsDistributionAxis }, + options?: Partial, +) => { + const { chainId } = useChainId(); + + return useQuery({ + queryKey: [ + FunctionKey.GET_RISK_DASHBOARD_LIQUIDATIONS_DISTRIBUTION, + { chainId, days, by }, + ], + queryFn: () => + getRiskDashboardLiquidationsDistribution({ chainId, days, by }), + ...options, + }); +}; diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsSummary/index.ts b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsSummary/index.ts new file mode 100644 index 0000000000..db4158b0de --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsSummary/index.ts @@ -0,0 +1,26 @@ +import type { ChainId } from 'types'; +import { fetchRiskDashboard } from '../fetchRiskDashboard'; + +export interface GetRiskDashboardLiquidationsSummaryResponse { + chainId: string; + days: number; + count: string; + debtRepaidUsdCents: string; + collateralSeizedUsdCents: string; + bonusUsdCents: string; + activeLiquidators: string; + uniqueBorrowers: string; +} + +export const getRiskDashboardLiquidationsSummary = ({ + chainId, + days, +}: { + chainId: ChainId; + days: number; +}) => + fetchRiskDashboard({ + endpoint: '/risk-dashboard/liquidations-summary', + chainId, + params: { days }, + }); diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsSummary/useGetRiskDashboardLiquidationsSummary.ts b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsSummary/useGetRiskDashboardLiquidationsSummary.ts new file mode 100644 index 0000000000..ce5e34e0fe --- /dev/null +++ b/apps/evm/src/clients/api/queries/getRiskDashboardLiquidationsSummary/useGetRiskDashboardLiquidationsSummary.ts @@ -0,0 +1,38 @@ +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 GetRiskDashboardLiquidationsSummaryResponse, + getRiskDashboardLiquidationsSummary, +} from '.'; + +export type UseGetRiskDashboardLiquidationsSummaryQueryKey = [ + FunctionKey.GET_RISK_DASHBOARD_LIQUIDATIONS_SUMMARY, + { chainId: ChainId; days: number }, +]; + +type Options = QueryObserverOptions< + GetRiskDashboardLiquidationsSummaryResponse, + Error, + GetRiskDashboardLiquidationsSummaryResponse, + GetRiskDashboardLiquidationsSummaryResponse, + UseGetRiskDashboardLiquidationsSummaryQueryKey +>; + +export const useGetRiskDashboardLiquidationsSummary = ( + { days }: { days: number }, + options?: Partial, +) => { + const { chainId } = useChainId(); + + return useQuery({ + queryKey: [ + FunctionKey.GET_RISK_DASHBOARD_LIQUIDATIONS_SUMMARY, + { chainId, days }, + ], + queryFn: () => getRiskDashboardLiquidationsSummary({ chainId, days }), + ...options, + }); +}; diff --git a/apps/evm/src/clients/api/queries/getRiskDashboardWalletAggregates/index.ts b/apps/evm/src/clients/api/queries/getRiskDashboardWalletAggregates/index.ts index 7206ba8de4..f1f30f6378 100644 --- a/apps/evm/src/clients/api/queries/getRiskDashboardWalletAggregates/index.ts +++ b/apps/evm/src/clients/api/queries/getRiskDashboardWalletAggregates/index.ts @@ -19,6 +19,7 @@ export interface GetRiskDashboardWalletAggregatesResponse { valueEligibleForLiquidationUsdCents: string; totalCollateralAtRiskUsdCents: string; totalBadDebtUsdCents: string; + walletsWithBadDebt: string; } const HEALTH_FACTOR_AT_RISK_DECIMAL = 1.125; diff --git a/apps/evm/src/constants/functionKey.ts b/apps/evm/src/constants/functionKey.ts index 4ce8245eba..f455ca98e6 100644 --- a/apps/evm/src/constants/functionKey.ts +++ b/apps/evm/src/constants/functionKey.ts @@ -106,6 +106,9 @@ enum FunctionKey { 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_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_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 cf233861be..b4d0899e2b 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1761,6 +1761,43 @@ } }, "illustrationAlt": "Stats illustration", + "liquidations": { + "dailyChart": { + "noData": "No liquidations in the selected period.", + "title": "Liquidations Activity", + "tooltip": { + "activeLiquidators": "Active Liquidators", + "collateralSeized": "Collateral Seized", + "count": "Liquidations", + "debtRepaid": "Debt Repaid" + }, + "unavailable": "Daily liquidations unavailable." + }, + "distributionChart": { + "byCollateralTitle": "Liquidations by Collateral Asset", + "byDebtTitle": "Liquidations by Debt Asset", + "noData": "No liquidations in the selected period.", + "tooltip": { + "count": "Count", + "value": "Value" + }, + "unavailable": "Distribution unavailable." + }, + "kpis": { + "activeLiquidators": "Active Liquidators", + "bonus": "Liquidation Bonus", + "collateralSeized": "Collateral Seized", + "count": "Liquidations", + "debtRepaid": "Debt Repaid", + "unavailable": "Liquidations data unavailable.", + "walletsWithBadDebt": "Wallets with Bad Debt" + }, + "period": { + "24h": "24h", + "30d": "30d", + "7d": "7d" + } + }, "marketKpis": { "liquidity": "Liquidity", "totalBorrows": "Total Borrows", diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index 043a3447b6..beec307d18 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1761,6 +1761,43 @@ } }, "illustrationAlt": "統計イラスト", + "liquidations": { + "dailyChart": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "tooltip": { + "activeLiquidators": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "distributionChart": { + "byCollateralTitle": "TRANSLATION NEEDED", + "byDebtTitle": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "tooltip": { + "count": "TRANSLATION NEEDED", + "value": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "kpis": { + "activeLiquidators": "TRANSLATION NEEDED", + "bonus": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED", + "walletsWithBadDebt": "TRANSLATION NEEDED" + }, + "period": { + "24h": "TRANSLATION NEEDED", + "30d": "TRANSLATION NEEDED", + "7d": "TRANSLATION NEEDED" + } + }, "marketKpis": { "liquidity": "TRANSLATION NEEDED", "totalBorrows": "TRANSLATION NEEDED", diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index 59b471b4cf..79753450f5 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1761,6 +1761,43 @@ } }, "illustrationAlt": "ภาพประกอบสถิติ", + "liquidations": { + "dailyChart": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "tooltip": { + "activeLiquidators": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "distributionChart": { + "byCollateralTitle": "TRANSLATION NEEDED", + "byDebtTitle": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "tooltip": { + "count": "TRANSLATION NEEDED", + "value": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "kpis": { + "activeLiquidators": "TRANSLATION NEEDED", + "bonus": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED", + "walletsWithBadDebt": "TRANSLATION NEEDED" + }, + "period": { + "24h": "TRANSLATION NEEDED", + "30d": "TRANSLATION NEEDED", + "7d": "TRANSLATION NEEDED" + } + }, "marketKpis": { "liquidity": "TRANSLATION NEEDED", "totalBorrows": "TRANSLATION NEEDED", diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index 9d968d6d25..fa707bc013 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1761,6 +1761,43 @@ } }, "illustrationAlt": "İstatistik illüstrasyonu", + "liquidations": { + "dailyChart": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "tooltip": { + "activeLiquidators": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "distributionChart": { + "byCollateralTitle": "TRANSLATION NEEDED", + "byDebtTitle": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "tooltip": { + "count": "TRANSLATION NEEDED", + "value": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "kpis": { + "activeLiquidators": "TRANSLATION NEEDED", + "bonus": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED", + "walletsWithBadDebt": "TRANSLATION NEEDED" + }, + "period": { + "24h": "TRANSLATION NEEDED", + "30d": "TRANSLATION NEEDED", + "7d": "TRANSLATION NEEDED" + } + }, "marketKpis": { "liquidity": "TRANSLATION NEEDED", "totalBorrows": "TRANSLATION NEEDED", diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index e10b2c01e0..f9855f7161 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1761,6 +1761,43 @@ } }, "illustrationAlt": "Hình minh họa thống kê", + "liquidations": { + "dailyChart": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "tooltip": { + "activeLiquidators": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "distributionChart": { + "byCollateralTitle": "TRANSLATION NEEDED", + "byDebtTitle": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "tooltip": { + "count": "TRANSLATION NEEDED", + "value": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "kpis": { + "activeLiquidators": "TRANSLATION NEEDED", + "bonus": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED", + "walletsWithBadDebt": "TRANSLATION NEEDED" + }, + "period": { + "24h": "TRANSLATION NEEDED", + "30d": "TRANSLATION NEEDED", + "7d": "TRANSLATION NEEDED" + } + }, "marketKpis": { "liquidity": "TRANSLATION NEEDED", "totalBorrows": "TRANSLATION NEEDED", diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index f8a27b5129..01e0f0893a 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1761,6 +1761,43 @@ } }, "illustrationAlt": "统计插图", + "liquidations": { + "dailyChart": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "tooltip": { + "activeLiquidators": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "distributionChart": { + "byCollateralTitle": "TRANSLATION NEEDED", + "byDebtTitle": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "tooltip": { + "count": "TRANSLATION NEEDED", + "value": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "kpis": { + "activeLiquidators": "TRANSLATION NEEDED", + "bonus": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED", + "walletsWithBadDebt": "TRANSLATION NEEDED" + }, + "period": { + "24h": "TRANSLATION NEEDED", + "30d": "TRANSLATION NEEDED", + "7d": "TRANSLATION NEEDED" + } + }, "marketKpis": { "liquidity": "TRANSLATION NEEDED", "totalBorrows": "TRANSLATION NEEDED", diff --git a/apps/evm/src/libs/translations/translations/zh-Hant.json b/apps/evm/src/libs/translations/translations/zh-Hant.json index dd0c6bab28..7aaff96bd7 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1761,6 +1761,43 @@ } }, "illustrationAlt": "統計插圖", + "liquidations": { + "dailyChart": { + "noData": "TRANSLATION NEEDED", + "title": "TRANSLATION NEEDED", + "tooltip": { + "activeLiquidators": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "distributionChart": { + "byCollateralTitle": "TRANSLATION NEEDED", + "byDebtTitle": "TRANSLATION NEEDED", + "noData": "TRANSLATION NEEDED", + "tooltip": { + "count": "TRANSLATION NEEDED", + "value": "TRANSLATION NEEDED" + }, + "unavailable": "TRANSLATION NEEDED" + }, + "kpis": { + "activeLiquidators": "TRANSLATION NEEDED", + "bonus": "TRANSLATION NEEDED", + "collateralSeized": "TRANSLATION NEEDED", + "count": "TRANSLATION NEEDED", + "debtRepaid": "TRANSLATION NEEDED", + "unavailable": "TRANSLATION NEEDED", + "walletsWithBadDebt": "TRANSLATION NEEDED" + }, + "period": { + "24h": "TRANSLATION NEEDED", + "30d": "TRANSLATION NEEDED", + "7d": "TRANSLATION NEEDED" + } + }, "marketKpis": { "liquidity": "TRANSLATION NEEDED", "totalBorrows": "TRANSLATION NEEDED", diff --git a/apps/evm/src/pages/Stats/Liquidations/LiquidationsDailyChart/index.tsx b/apps/evm/src/pages/Stats/Liquidations/LiquidationsDailyChart/index.tsx new file mode 100644 index 0000000000..0bfe5aaf44 --- /dev/null +++ b/apps/evm/src/pages/Stats/Liquidations/LiquidationsDailyChart/index.tsx @@ -0,0 +1,149 @@ +import { theme } from '@venusprotocol/ui'; +import { + type ApiRiskDashboardLiquidationsDailyDay, + useGetRiskDashboardLiquidationsDaily, +} from 'clients/api'; +import { Card, Spinner } from 'components'; +import { useTranslation } from 'libs/translations'; +import { + Bar, + CartesianGrid, + BarChart as RCBarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { formatCentsToReadableValue } from 'utilities'; +import { roundUpToScale } from '../../roundUpToScale'; + +interface ChartDatum { + day: string; + count: number; + debtRepaidUsdCents: string; + collateralSeizedUsdCents: string; + activeLiquidators: string; +} + +const formatDayShort = (day: string) => { + const date = new Date(`${day}T00:00:00Z`); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); +}; + +const buildChartData = (series: ApiRiskDashboardLiquidationsDailyDay[]): ChartDatum[] => + series.map(day => ({ + day: day.day, + count: Number(day.count), + debtRepaidUsdCents: day.debtRepaidUsdCents, + collateralSeizedUsdCents: day.collateralSeizedUsdCents, + activeLiquidators: day.activeLiquidators, + })); + +interface TooltipPayloadItem { + payload: ChartDatum; +} + +const DailyTooltip: 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 ( +
+

{formatDayShort(datum.day)}

+
+ + {t('statsPage.liquidations.dailyChart.tooltip.count')} + + {datum.count} +
+
+ + {t('statsPage.liquidations.dailyChart.tooltip.debtRepaid')} + + + {formatCentsToReadableValue({ value: datum.debtRepaidUsdCents })} + +
+
+ + {t('statsPage.liquidations.dailyChart.tooltip.collateralSeized')} + + + {formatCentsToReadableValue({ value: datum.collateralSeizedUsdCents })} + +
+
+ + {t('statsPage.liquidations.dailyChart.tooltip.activeLiquidators')} + + {datum.activeLiquidators} +
+
+ ); +}; + +export interface LiquidationsDailyChartProps { + days: number; +} + +export const LiquidationsDailyChart: React.FC = ({ days }) => { + const { t } = useTranslation(); + const { data, isLoading, isError } = useGetRiskDashboardLiquidationsDaily({ days }); + const chartData = data ? buildChartData(data.series) : []; + + return ( + +

{t('statsPage.liquidations.dailyChart.title')}

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

{t('statsPage.liquidations.dailyChart.unavailable')}

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

{t('statsPage.liquidations.dailyChart.noData')}

+ ) : ( + + + + + + + + + } + /> + + + + + )} +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/Liquidations/LiquidationsDistributionChart/index.tsx b/apps/evm/src/pages/Stats/Liquidations/LiquidationsDistributionChart/index.tsx new file mode 100644 index 0000000000..6a81e1441b --- /dev/null +++ b/apps/evm/src/pages/Stats/Liquidations/LiquidationsDistributionChart/index.tsx @@ -0,0 +1,187 @@ +import { theme } from '@venusprotocol/ui'; +import { + type ApiRiskDashboardLiquidationsDistributionRow, + type LiquidationsDistributionAxis, + useGetRiskDashboardLiquidationsDistribution, +} 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 { + Bar, + CartesianGrid, + Cell, + BarChart as RCBarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { formatCentsToReadableValue, truncateAddress } from 'utilities'; +import { getAddress } from 'viem'; +import { roundUpToScale } from '../../roundUpToScale'; +import { useMarketColors } from '../../useMarketColors'; + +interface ChartDatum { + marketAddress: string; + symbol: string; + valueDollars: number; + count: string; + valueUsdCents: string; +} + +const toDollars = (cents: string) => Number(cents) / 100; + +const buildChartData = ( + rows: ApiRiskDashboardLiquidationsDistributionRow[], + symbolByMarket: Map, +): ChartDatum[] => + [...rows] + .sort((a, b) => Number(b.valueUsdCents) - Number(a.valueUsdCents)) + .map(row => ({ + marketAddress: row.marketAddress, + symbol: symbolByMarket.get(row.marketAddress) ?? truncateAddress(row.marketAddress), + valueDollars: toDollars(row.valueUsdCents), + count: row.count, + valueUsdCents: row.valueUsdCents, + })); + +interface TooltipPayloadItem { + payload: ChartDatum; +} + +const DistributionTooltip: 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 ( +
+

{datum.symbol}

+
+ + {t('statsPage.liquidations.distributionChart.tooltip.value')} + + + {formatCentsToReadableValue({ value: datum.valueUsdCents })} + +
+
+ + {t('statsPage.liquidations.distributionChart.tooltip.count')} + + {datum.count} +
+
+ ); +}; + +export interface LiquidationsDistributionChartProps { + days: number; + by: LiquidationsDistributionAxis; +} + +export const LiquidationsDistributionChart: React.FC = ({ + days, + by, +}) => { + const { t } = useTranslation(); + const vTokens = useGetVTokens(); + const { data, isLoading, isError } = useGetRiskDashboardLiquidationsDistribution({ days, by }); + const { colorByMarket } = useMarketColors(); + + const symbolByMarket = useMemo(() => { + const map = new Map(); + for (const vToken of vTokens) { + map.set(getAddress(vToken.address), vToken.underlyingToken.symbol); + } + return map; + }, [vTokens]); + + const chartData = useMemo( + () => (data ? buildChartData(data.rows, symbolByMarket) : []), + [data, symbolByMarket], + ); + + // t('statsPage.liquidations.distributionChart.byCollateralTitle') + // t('statsPage.liquidations.distributionChart.byDebtTitle') + const title = t( + by === 'collateral' + ? 'statsPage.liquidations.distributionChart.byCollateralTitle' + : 'statsPage.liquidations.distributionChart.byDebtTitle', + ); + + return ( + +

{title}

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

{t('statsPage.liquidations.distributionChart.unavailable')}

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

{t('statsPage.liquidations.distributionChart.noData')}

+ ) : ( + + + + + + formatCentsToReadableValue({ value: Math.round(value * 100) }) + } + /> + + + + } + /> + + + {chartData.map(datum => ( + + ))} + + + + )} +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/Liquidations/LiquidationsKpis/index.tsx b/apps/evm/src/pages/Stats/Liquidations/LiquidationsKpis/index.tsx new file mode 100644 index 0000000000..daad67c7c2 --- /dev/null +++ b/apps/evm/src/pages/Stats/Liquidations/LiquidationsKpis/index.tsx @@ -0,0 +1,76 @@ +import { + useGetRiskDashboardLiquidationsSummary, + useGetRiskDashboardWalletAggregates, +} from 'clients/api'; +import { Card, Spinner } from 'components'; +import { useTranslation } from 'libs/translations'; +import { formatCentsToReadableValue } from 'utilities'; + +interface KpiCellProps { + label: string; + value: string; +} + +const KpiCell: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +const formatInteger = (value: string) => Number.parseInt(value, 10).toLocaleString('en-US'); + +export interface LiquidationsKpisProps { + days: number; +} + +export const LiquidationsKpis: React.FC = ({ days }) => { + const { t } = useTranslation(); + const summary = useGetRiskDashboardLiquidationsSummary({ days }); + const walletAggregates = useGetRiskDashboardWalletAggregates(); + + if (summary.isLoading || walletAggregates.isLoading) { + return ( +
+ +
+ ); + } + + if (summary.isError || !summary.data) { + return

{t('statsPage.liquidations.kpis.unavailable')}

; + } + + return ( + +
+ + + + + + +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/Liquidations/index.tsx b/apps/evm/src/pages/Stats/Liquidations/index.tsx new file mode 100644 index 0000000000..c5d2ed2ba0 --- /dev/null +++ b/apps/evm/src/pages/Stats/Liquidations/index.tsx @@ -0,0 +1,39 @@ +import { ButtonGroup } from 'components'; +import { useTranslation } from 'libs/translations'; +import { useState } from 'react'; +import { LiquidationsDailyChart } from './LiquidationsDailyChart'; +import { LiquidationsDistributionChart } from './LiquidationsDistributionChart'; +import { LiquidationsKpis } from './LiquidationsKpis'; + +// t('statsPage.liquidations.period.24h') +// t('statsPage.liquidations.period.7d') +// t('statsPage.liquidations.period.30d') +const PERIOD_OPTIONS = [ + { days: 1, labelKey: 'statsPage.liquidations.period.24h' }, + { days: 7, labelKey: 'statsPage.liquidations.period.7d' }, + { days: 30, labelKey: 'statsPage.liquidations.period.30d' }, +] as const; +const DEFAULT_PERIOD_INDEX = PERIOD_OPTIONS.length - 1; + +export const Liquidations: React.FC = () => { + const { t } = useTranslation(); + const [activeIndex, setActiveIndex] = useState(DEFAULT_PERIOD_INDEX); + const days = PERIOD_OPTIONS[activeIndex].days; + + return ( +
+ t(option.labelKey))} + activeButtonIndex={activeIndex} + onButtonClick={setActiveIndex} + /> + + +
+ + +
+
+ ); +}; diff --git a/apps/evm/src/pages/Stats/index.tsx b/apps/evm/src/pages/Stats/index.tsx index 2ef3276835..bec0250fa5 100644 --- a/apps/evm/src/pages/Stats/index.tsx +++ b/apps/evm/src/pages/Stats/index.tsx @@ -2,6 +2,7 @@ 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'; @@ -53,7 +54,7 @@ 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: null }, + { id: 'liquidations', title: t('statsPage.tabs.liquidations'), content: }, ]} />