From 2c4836c1d91f99b74a6fedbe373f167e5a13e673 Mon Sep 17 00:00:00 2001 From: Bran <52735957+brancoder@users.noreply.github.com> Date: Thu, 28 Mar 2024 14:58:10 +0100 Subject: [PATCH] Feat: Add blocks and tx count to slot feed (#1346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Slot Analytics influx * fix: add slot stats to landing page + update useEpochStats + fix burnedManaCache * fix: display 0 if stat undefined * fix: burned mana cell * fix: change no_payload_count to validation_count * hotfix: slots feed links not absolute --------- Co-authored-by: BegoƱa Alvarez --- .../stats/slot/ISlotAnalyticStatsRequest.ts | 11 ++ .../stats/slot/ISlotAnalyticStatsResponse.ts | 12 ++ api/src/models/influx/nova/IInfluxDbCache.ts | 16 +++ api/src/routes.ts | 6 + api/src/routes/nova/epoch/influx/get.ts | 6 +- api/src/routes/nova/slot/blocks/stats/get.ts | 42 ++++++ api/src/services/nova/influx/influxQueries.ts | 4 +- .../services/nova/influx/influxServiceNova.ts | 126 +++++++++++++++++- .../components/nova/landing/SlotTableCell.tsx | 28 +++- client/src/app/routes/nova/EpochPage.tsx | 2 +- .../src/helpers/nova/hooks/useEpochStats.ts | 4 +- .../nova/hooks/useGenerateSlotsTable.ts | 22 +-- client/src/helpers/nova/hooks/useSlotStats.ts | 48 +++++++ .../nova/stats/ISlotAnalyticStatsRequest.ts | 11 ++ .../nova/stats/ISlotAnalyticStatsResponse.ts | 12 ++ client/src/services/nova/novaApiClient.ts | 11 ++ 16 files changed, 332 insertions(+), 29 deletions(-) create mode 100644 api/src/models/api/nova/stats/slot/ISlotAnalyticStatsRequest.ts create mode 100644 api/src/models/api/nova/stats/slot/ISlotAnalyticStatsResponse.ts create mode 100644 api/src/routes/nova/slot/blocks/stats/get.ts create mode 100644 client/src/helpers/nova/hooks/useSlotStats.ts create mode 100644 client/src/models/api/nova/stats/ISlotAnalyticStatsRequest.ts create mode 100644 client/src/models/api/nova/stats/ISlotAnalyticStatsResponse.ts diff --git a/api/src/models/api/nova/stats/slot/ISlotAnalyticStatsRequest.ts b/api/src/models/api/nova/stats/slot/ISlotAnalyticStatsRequest.ts new file mode 100644 index 000000000..22434e5e6 --- /dev/null +++ b/api/src/models/api/nova/stats/slot/ISlotAnalyticStatsRequest.ts @@ -0,0 +1,11 @@ +export interface ISlotAnalyticStatsRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The slot index to get the stats for. + */ + slotIndex: string; +} diff --git a/api/src/models/api/nova/stats/slot/ISlotAnalyticStatsResponse.ts b/api/src/models/api/nova/stats/slot/ISlotAnalyticStatsResponse.ts new file mode 100644 index 000000000..991f34ef8 --- /dev/null +++ b/api/src/models/api/nova/stats/slot/ISlotAnalyticStatsResponse.ts @@ -0,0 +1,12 @@ +import { IResponse } from "../../IResponse"; + +export interface ISlotAnalyticStatsResponse extends IResponse { + slotIndex?: number; + blockCount?: number; + perPayloadType?: { + transaction?: number; + candidacy?: number; + taggedData?: number; + noPayload?: number; + }; +} diff --git a/api/src/models/influx/nova/IInfluxDbCache.ts b/api/src/models/influx/nova/IInfluxDbCache.ts index 1d65c68ef..2ad3cb02c 100644 --- a/api/src/models/influx/nova/IInfluxDbCache.ts +++ b/api/src/models/influx/nova/IInfluxDbCache.ts @@ -74,6 +74,17 @@ interface IEpochAnalyticStats { }; } +interface ISlotAnalyticStats { + slotIndex: number; + blockCount: number; + perPayloadType: { + transaction: number; + taggedData: number; + candidacy: number; + noPayload: number; + }; +} + /** * The epoch stats cache. Map epoch index to stats. */ @@ -89,6 +100,11 @@ export type ManaBurnedInSlot = ITimedEntry & { */ export type ManaBurnedInSlotCache = Map; +/** + * The slot stats cache. Map slot index to stats. + */ +export type IInfluxSlotAnalyticsCache = Map; + /** * The helper to initialize empty maps * @returns The initial cache object diff --git a/api/src/routes.ts b/api/src/routes.ts index bab14d6a6..65fc08ad9 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -336,4 +336,10 @@ export const routes: IRoute[] = [ folder: "nova/epoch/influx", func: "get", }, + { + path: "/nova/slot/stats/:network/:slotIndex", + method: "get", + folder: "nova/slot/blocks/stats", + func: "get", + }, ]; diff --git a/api/src/routes/nova/epoch/influx/get.ts b/api/src/routes/nova/epoch/influx/get.ts index 5287eb67f..4d03db6c5 100644 --- a/api/src/routes/nova/epoch/influx/get.ts +++ b/api/src/routes/nova/epoch/influx/get.ts @@ -6,7 +6,6 @@ import { IConfiguration } from "../../../../models/configuration/IConfiguration" import { NOVA } from "../../../../models/db/protocolVersion"; import { NetworkService } from "../../../../services/networkService"; import { InfluxServiceNova } from "../../../../services/nova/influx/influxServiceNova"; -import { NodeInfoService } from "../../../../services/nova/nodeInfoService"; import { ValidationHelper } from "../../../../utils/validationHelper"; /** @@ -33,10 +32,7 @@ export async function get(_: IConfiguration, request: IEpochAnalyticStatsRequest } const influxService = ServiceFactory.get(`influxdb-${request.network}`); - const nodeService = ServiceFactory.get(`node-info-${request.network}`); - const protocolParameters = await nodeService.getProtocolParameters(); - - if (!influxService || !protocolParameters) { + if (!influxService) { return { error: "Influx service not found for this network." }; } diff --git a/api/src/routes/nova/slot/blocks/stats/get.ts b/api/src/routes/nova/slot/blocks/stats/get.ts new file mode 100644 index 000000000..d0c267de0 --- /dev/null +++ b/api/src/routes/nova/slot/blocks/stats/get.ts @@ -0,0 +1,42 @@ +import { ServiceFactory } from "../../../../../factories/serviceFactory"; +import { ISlotAnalyticStatsRequest } from "../../../../../models/api/nova/stats/slot/ISlotAnalyticStatsRequest"; +import { ISlotAnalyticStatsResponse } from "../../../../../models/api/nova/stats/slot/ISlotAnalyticStatsResponse"; +import { IConfiguration } from "../../../../../models/configuration/IConfiguration"; +import { NOVA } from "../../../../../models/db/protocolVersion"; +import { NetworkService } from "../../../../../services/networkService"; +import { InfluxServiceNova } from "../../../../../services/nova/influx/influxServiceNova"; +import { ValidationHelper } from "../../../../../utils/validationHelper"; + +/** + * Fetch the slot stats from influx nova. + * @param _ The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(_: IConfiguration, request: ISlotAnalyticStatsRequest): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.numberFromString(request.slotIndex, "slotIndex"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + const influxService = ServiceFactory.get(`influxdb-${networkConfig.network}`); + + if (influxService) { + const slotIndex = Number.parseInt(request.slotIndex, 10); + const slotStats = await influxService.getSlotAnalyticStats(slotIndex); + + if (slotStats) { + return slotStats; + } + + return { error: `Could not fetch stats for slot ${request.slotIndex}` }; + } + + return { error: "Influx service not found for this network." }; +} diff --git a/api/src/services/nova/influx/influxQueries.ts b/api/src/services/nova/influx/influxQueries.ts index 6ad8f4607..7b3abc2e3 100644 --- a/api/src/services/nova/influx/influxQueries.ts +++ b/api/src/services/nova/influx/influxQueries.ts @@ -86,12 +86,12 @@ export const OUTPUTS_DAILY_QUERY = { `, }; -export const EPOCH_STATS_QUERY_BY_EPOCH_INDEX = ` +export const BLOCK_STATS_QUERY = ` SELECT sum("transaction_count") AS "transaction", sum("tagged_data_count") AS "taggedData", sum("candidacy_announcement_count") AS "candidacy", - sum("no_payload_count") AS "noPayload" + sum("validation_count") AS "noPayload" FROM "iota_block_activity" WHERE time >= $from and time <= $to `; diff --git a/api/src/services/nova/influx/influxServiceNova.ts b/api/src/services/nova/influx/influxServiceNova.ts index 8d0b6e4e1..8c0ce8d2c 100644 --- a/api/src/services/nova/influx/influxServiceNova.ts +++ b/api/src/services/nova/influx/influxServiceNova.ts @@ -29,8 +29,8 @@ import { TRANSACTION_DAILY_QUERY, UNLOCK_CONDITIONS_PER_TYPE_DAILY_QUERY, VALIDATORS_ACTIVITY_DAILY_QUERY, - EPOCH_STATS_QUERY_BY_EPOCH_INDEX, DELEGATORS_TOTAL_QUERY, + BLOCK_STATS_QUERY, } from "./influxQueries"; import { ServiceFactory } from "../../../factories/serviceFactory"; import logger from "../../../logger"; @@ -39,6 +39,7 @@ import { IInfluxAnalyticsCache, IInfluxDailyCache, IInfluxEpochAnalyticsCache, + IInfluxSlotAnalyticsCache, initializeEmptyDailyCache, ManaBurnedInSlot, ManaBurnedInSlotCache, @@ -79,12 +80,25 @@ type EpochUpdate = ITimedEntry & { noPayload: number; }; +type SlotUpdate = ITimedEntry & { + slotIndex: number; + taggedData: number; + candidacy: number; + transaction: number; + noPayload: number; +}; + /** * Epoch analyitics cache MAX size. */ const EPOCH_CACHE_MAX = 20; /** + * Slot analyitics cache MAX size. + */ +const SLOT_CACHE_MAX = 200; + +/* * Epoch analyitics cache MAX size. */ const MANA_BURNED_CACHE_MAX = 20; @@ -107,6 +121,12 @@ const COLLECT_ANALYTICS_DATA_CRON = "55 58 * * * *"; */ const COLLECT_EPOCH_ANALYTICS_DATA_CRON = "*/10 * * * *"; +/** + * The collect slot analytics data interval cron expression. + * Every 10 seconds + */ +const COLLECT_SLOT_ANALYTICS_DATA_CRON = "*/10 * * * * *"; + export class InfluxServiceNova extends InfluxDbClient { /** * The InfluxDb Client. @@ -123,6 +143,11 @@ export class InfluxServiceNova extends InfluxDbClient { */ protected readonly _epochCache: IInfluxEpochAnalyticsCache; + /** + * The current influx slot analytics cache instance. + */ + protected readonly _slotCache: IInfluxSlotAnalyticsCache; + /** * The current influx analytics cache instance. */ @@ -148,6 +173,7 @@ export class InfluxServiceNova extends InfluxDbClient { this._novatimeService = ServiceFactory.get(`nova-time-${network.network}`); this._dailyCache = initializeEmptyDailyCache(); this._epochCache = new Map(); + this._slotCache = new Map(); this._manaBurnedInSlotCache = new Map(); this._analyticsCache = {}; } @@ -268,6 +294,14 @@ export class InfluxServiceNova extends InfluxDbClient { return this._epochCache.get(epochIndex); } + public async getSlotAnalyticStats(slotIndex: number) { + if (!this._slotCache.get(slotIndex)) { + await this.collectSlotStatsByIndex(slotIndex); + } + + return this._slotCache.get(slotIndex); + } + /** * Get the manaBurned stats by slot index. * @param slotIndex - The slot index. @@ -311,6 +345,8 @@ export class InfluxServiceNova extends InfluxDbClient { void this.collectAnalytics(); // eslint-disable-next-line no-void void this.collectEpochStats(); + // eslint-disable-next-line no-void + void this.collectSlotStats(); if (this._client) { cron.schedule(COLLECT_GRAPHS_DATA_CRON, async () => { @@ -327,6 +363,11 @@ export class InfluxServiceNova extends InfluxDbClient { // eslint-disable-next-line no-void void this.collectEpochStats(); }); + + cron.schedule(COLLECT_SLOT_ANALYTICS_DATA_CRON, async () => { + // eslint-disable-next-line no-void + void this.collectSlotStats(); + }); } } @@ -485,7 +526,7 @@ export class InfluxServiceNova extends InfluxDbClient { const fromNano = toNanoDate((moment(Number(from) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString()); const toNano = toNanoDate((moment(Number(to) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString()); - await this.queryInflux(EPOCH_STATS_QUERY_BY_EPOCH_INDEX, fromNano, toNano) + await this.queryInflux(BLOCK_STATS_QUERY, fromNano, toNano) .then((results) => { for (const update of results) { update.epochIndex = epochIndex; @@ -493,9 +534,7 @@ export class InfluxServiceNova extends InfluxDbClient { } }) .catch((e) => { - logger.warn( - `[InfluxClient] Query ${EPOCH_STATS_QUERY_BY_EPOCH_INDEX} failed for (${this._network.network}). Cause ${e}`, - ); + logger.warn(`[InfluxClient] Query ${BLOCK_STATS_QUERY} failed for (${this._network.network}). Cause ${e}`); }); } catch (err) { logger.warn(`[InfluxNova] Failed refreshing epoch stats for "${this._network.network}". Cause: ${err}`); @@ -574,7 +613,82 @@ export class InfluxServiceNova extends InfluxDbClient { logger.debug(`[InfluxNova] Deleting epoch index "${lowestIndex}" ("${this._network.network}")`); - this._epochCache.delete(lowestIndex); + this._manaBurnedInSlotCache.delete(lowestIndex); + } + } + } + + /** + * Get the slot analytics by index and set it in the cache. + * @param slotIndex - The slot index. + */ + private async collectSlotStatsByIndex(slotIndex: number) { + try { + const { from, to } = this._novatimeService.getSlotIndexToUnixTimeRange(slotIndex); + const fromNano = toNanoDate((moment(Number(from) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString()); + const toNano = toNanoDate((moment(Number(to) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString()); + + await this.queryInflux(BLOCK_STATS_QUERY, fromNano, toNano) + .then((results) => { + for (const update of results) { + update.slotIndex = slotIndex; + this.updateSlotCache(update); + } + }) + .catch((e) => { + logger.warn(`[InfluxClient] Query ${BLOCK_STATS_QUERY} failed for (${this._network.network}). Cause ${e}`); + }); + } catch (err) { + logger.warn(`[InfluxNova] Failed refreshing slot stats for "${this._network.network}". Cause: ${err}`); + } + } + + /** + * Get the slot analytics and set it in the cache. + */ + private async collectSlotStats() { + try { + logger.debug(`[InfluxNova] Collecting slot stats for "${this._network.network}"`); + const slotIndex = this._novatimeService.getUnixTimestampToSlotIndex(moment().unix()); + // eslint-disable-next-line no-void + void this.collectSlotStatsByIndex(slotIndex); + } catch (err) { + logger.warn(`[InfluxNova] Failed refreshing slot stats for "${this._network.network}". Cause: ${err}`); + } + } + + private updateSlotCache(update: SlotUpdate) { + if (update.slotIndex !== undefined && !this._slotCache.has(update.slotIndex)) { + const { slotIndex, transaction, candidacy, taggedData, noPayload } = update; + const blockCount = transaction + candidacy + taggedData + noPayload; + this._slotCache.set(slotIndex, { + slotIndex, + blockCount, + perPayloadType: { + transaction, + candidacy, + taggedData, + noPayload, + }, + }); + + logger.debug(`[InfluxNova] Added slot index "${slotIndex}" to cache for "${this._network.network}"`); + + if (this._slotCache.size > SLOT_CACHE_MAX) { + let lowestIndex: number; + for (const index of this._slotCache.keys()) { + if (!lowestIndex) { + lowestIndex = index; + } + + if (slotIndex < lowestIndex) { + lowestIndex = index; + } + } + + logger.debug(`[InfluxNova] Deleting slot index "${lowestIndex}" ("${this._network.network}")`); + + this._slotCache.delete(lowestIndex); } } } diff --git a/client/src/app/components/nova/landing/SlotTableCell.tsx b/client/src/app/components/nova/landing/SlotTableCell.tsx index 67798dc4c..4d0af83c8 100644 --- a/client/src/app/components/nova/landing/SlotTableCell.tsx +++ b/client/src/app/components/nova/landing/SlotTableCell.tsx @@ -5,6 +5,7 @@ import StatusPill from "../StatusPill"; import TruncatedId from "../../stardust/TruncatedId"; import classNames from "classnames"; import { useSlotManaBurned } from "~/helpers/nova/hooks/useSlotManaBurned"; +import { useSlotStats } from "~/helpers/nova/hooks/useSlotStats"; import Spinner from "../../Spinner"; import { Link } from "react-router-dom"; @@ -13,11 +14,12 @@ export enum SlotTableCellType { Link = "link", Text = "text", TruncatedId = "truncated-id", + Stats = "stats", BurnedMana = "burned-mana", Empty = "empty", } -export type TSlotTableData = IPillStatusCell | ITextCell | ILinkCell | ITruncatedIdCell | IBurnedManaCell | IEmptyCell; +export type TSlotTableData = IPillStatusCell | ITextCell | ILinkCell | ITruncatedIdCell | IStatsCell | IBurnedManaCell | IEmptyCell; export default function SlotTableCellWrapper(cellData: TSlotTableData): React.JSX.Element { let Component: React.JSX.Element; @@ -35,6 +37,9 @@ export default function SlotTableCellWrapper(cellData: TSlotTableData): React.JS case SlotTableCellType.Empty: Component = ; break; + case SlotTableCellType.Stats: + Component = ; + break; case SlotTableCellType.BurnedMana: Component = ; break; @@ -76,6 +81,23 @@ function LinkCell({ data, href }: ILinkCell): React.JSX.Element { return {data}; } +interface IStatsCell { + data: string; + type: SlotTableCellType.Stats; + href: string; + statsType: "blocks" | "transactions"; + shouldLoad?: boolean; +} + +function StatsCell({ data, href, shouldLoad, statsType }: IStatsCell): React.JSX.Element { + const [slotStats, isLoading] = useSlotStats(shouldLoad ? data : null); + if (!shouldLoad) { + return ; + } + const stat = statsType === "blocks" ? slotStats?.blockCount : slotStats?.perPayloadType?.transaction; + return {isLoading ? : stat ? {stat} : "0"}; +} + interface ITextCell { data: string; type: SlotTableCellType.Text; @@ -107,12 +129,10 @@ interface IBurnedManaCell { } function BurnedManaCell({ data, shouldLoad }: IBurnedManaCell): React.JSX.Element { + const { slotManaBurned, isLoading } = useSlotManaBurned(shouldLoad ? data : null); if (!shouldLoad) { return ; } - - const { slotManaBurned, isLoading } = useSlotManaBurned(data); - return {isLoading ? : slotManaBurned?.manaBurned ?? "--"}; } diff --git a/client/src/app/routes/nova/EpochPage.tsx b/client/src/app/routes/nova/EpochPage.tsx index 447c1cfa5..1cde35773 100644 --- a/client/src/app/routes/nova/EpochPage.tsx +++ b/client/src/app/routes/nova/EpochPage.tsx @@ -32,7 +32,7 @@ const EpochPage: React.FC> = ({ const { epochUnixTimeRange, epochProgressPercent, registrationTime } = useEpochProgress(Number(epochIndex)); const { epochUnixTimeRange: currentEpochUnixTimeRange } = useEpochProgress(); const { epochCommittee } = useEpochCommittee(network, epochIndex); - const [epochStats] = useEpochStats(network, epochIndex); + const [epochStats] = useEpochStats(epochIndex); const { validators: candidates } = useValidators(); if ( diff --git a/client/src/helpers/nova/hooks/useEpochStats.ts b/client/src/helpers/nova/hooks/useEpochStats.ts index 5f7377e54..7342229aa 100644 --- a/client/src/helpers/nova/hooks/useEpochStats.ts +++ b/client/src/helpers/nova/hooks/useEpochStats.ts @@ -4,6 +4,7 @@ import { ServiceFactory } from "~factories/serviceFactory"; import { NOVA } from "~models/config/protocolVersion"; import { NovaApiClient } from "~/services/nova/novaApiClient"; import { IEpochAnalyticStats } from "~/models/api/nova/stats/IEpochAnalyticStats"; +import { useNetworkInfoNova } from "../networkInfo"; /** * Fetch the epoch stats @@ -11,7 +12,8 @@ import { IEpochAnalyticStats } from "~/models/api/nova/stats/IEpochAnalyticStats * @param epochIndex The epoch index * @returns The epoch stats and a loading bool. */ -export function useEpochStats(network: string, epochIndex: string | null): [IEpochAnalyticStats | null, boolean] { +export function useEpochStats(epochIndex: string | null): [IEpochAnalyticStats | null, boolean] { + const { name: network } = useNetworkInfoNova((s) => s.networkInfo); const isMounted = useIsMounted(); const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); const [epochStats, setEpochStats] = useState(null); diff --git a/client/src/helpers/nova/hooks/useGenerateSlotsTable.ts b/client/src/helpers/nova/hooks/useGenerateSlotsTable.ts index a2528f860..983151bae 100644 --- a/client/src/helpers/nova/hooks/useGenerateSlotsTable.ts +++ b/client/src/helpers/nova/hooks/useGenerateSlotsTable.ts @@ -29,7 +29,7 @@ function getSlotIndexTableCell(network: string, slotIndex: number, slotTimeRange return { type: SlotTableCellType.Link, data: slotIndexString, - href: `${network}/slot/${slotIndex}`, + href: `/${network}/slot/${slotIndex}`, }; } return { @@ -102,8 +102,6 @@ function getSlotCommitmentTableRow( const slotCommitmentId = Utils.computeSlotCommitmentId(commitmentWrapper.slotCommitment); const referenceManaCost = commitmentWrapper.slotCommitment.referenceManaCost.toString(); - const blocks = "2000"; - const transactions = "2000"; const slotStatus = commitmentWrapper.status; Object.values(SlotTableHeadings).forEach((heading) => { @@ -117,7 +115,7 @@ function getSlotCommitmentTableRow( tableData = { type: SlotTableCellType.TruncatedId, data: slotCommitmentId, - href: `${network}/slot/${slotIndex}`, + href: `/${network}/slot/${slotIndex}`, }; break; case SlotTableHeadings.ReferenceManaCost: @@ -128,16 +126,20 @@ function getSlotCommitmentTableRow( break; case SlotTableHeadings.Blocks: tableData = { - type: SlotTableCellType.Link, - data: blocks, - href: `${network}/slot/${slotIndex}?tab=RefBlocks`, + type: SlotTableCellType.Stats, + data: slotIndex.toString(), + href: `/${network}/slot/${slotIndex}?tab=RefBlocks`, + statsType: "blocks", + shouldLoad: slotStatus === SlotCommitmentStatus.Finalized, }; break; case SlotTableHeadings.Txs: tableData = { - type: SlotTableCellType.Link, - data: transactions, - href: `${network}/slot/${slotIndex}?tab=Txs`, + type: SlotTableCellType.Stats, + data: slotIndex.toString(), + href: `/${network}/slot/${slotIndex}?tab=Txs`, + statsType: "transactions", + shouldLoad: slotStatus === SlotCommitmentStatus.Finalized, }; break; case SlotTableHeadings.BurnedMana: diff --git a/client/src/helpers/nova/hooks/useSlotStats.ts b/client/src/helpers/nova/hooks/useSlotStats.ts new file mode 100644 index 000000000..99d7b3519 --- /dev/null +++ b/client/src/helpers/nova/hooks/useSlotStats.ts @@ -0,0 +1,48 @@ +import { useEffect, useState } from "react"; +import { useIsMounted } from "~helpers/hooks/useIsMounted"; +import { ServiceFactory } from "~factories/serviceFactory"; +import { NOVA } from "~models/config/protocolVersion"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; +import { useNetworkInfoNova } from "../networkInfo"; +import { ISlotAnalyticStatsResponse } from "~/models/api/nova/stats/ISlotAnalyticStatsResponse"; + +/** + * Fetch the slot stats + * @param network The Network in context + * @param slotIndex The slot index + * @returns The slot stats and a loading bool. + */ +export function useSlotStats(slotIndex: string | null): [ISlotAnalyticStatsResponse | null, boolean] { + const { name: network } = useNetworkInfoNova((s) => s.networkInfo); + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [slotStats, setSlotStats] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + setSlotStats(null); + if (slotIndex) { + // eslint-disable-next-line no-void + void (async () => { + apiClient + .slotStats({ + network, + slotIndex, + }) + .then((response) => { + if (isMounted) { + setSlotStats(response ?? null); + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, slotIndex]); + + return [slotStats, isLoading]; +} diff --git a/client/src/models/api/nova/stats/ISlotAnalyticStatsRequest.ts b/client/src/models/api/nova/stats/ISlotAnalyticStatsRequest.ts new file mode 100644 index 000000000..22434e5e6 --- /dev/null +++ b/client/src/models/api/nova/stats/ISlotAnalyticStatsRequest.ts @@ -0,0 +1,11 @@ +export interface ISlotAnalyticStatsRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The slot index to get the stats for. + */ + slotIndex: string; +} diff --git a/client/src/models/api/nova/stats/ISlotAnalyticStatsResponse.ts b/client/src/models/api/nova/stats/ISlotAnalyticStatsResponse.ts new file mode 100644 index 000000000..671b8c039 --- /dev/null +++ b/client/src/models/api/nova/stats/ISlotAnalyticStatsResponse.ts @@ -0,0 +1,12 @@ +import { IResponse } from "../IResponse"; + +export interface ISlotAnalyticStatsResponse extends IResponse { + slotIndex?: number; + blockCount?: number; + perPayloadType?: { + transaction?: number; + candidacy?: number; + taggedData?: number; + noPayload?: number; + }; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index 7a3e34eaa..edaf87cb3 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -58,6 +58,8 @@ import { IValidatorStatsResponse } from "~/models/api/nova/IValidatorStatsRespon import { IDelegationByValidatorResponse } from "~/models/api/nova/IDelegationByValidatorResponse"; import { ISlotManaBurnedRequest } from "~/models/api/nova/stats/ISlotManaBurnedRequest"; import { ISlotManaBurnedResponse } from "~/models/api/nova/stats/ISlotManaBurnedResponse"; +import { ISlotAnalyticStatsRequest } from "~/models/api/nova/stats/ISlotAnalyticStatsRequest"; +import { ISlotAnalyticStatsResponse } from "~/models/api/nova/stats/ISlotAnalyticStatsResponse"; /** * Class to handle api communications on nova. @@ -321,6 +323,15 @@ export class NovaApiClient extends ApiClient { return this.callApi(`nova/epoch/committee/${request.network}/${request.epochIndex}`, "get"); } + /** + * Get the slot analytics stats by slot index. + * @param request The slot analytic stats get request. + * @returns The slot stats response. + */ + public async slotStats(request: ISlotAnalyticStatsRequest): Promise { + return this.callApi(`nova/slot/stats/${request.network}/${request.slotIndex}`, "get"); + } + /** * Get the epoch analytics stats by epoch index. * @param request The epoch analytic stats get request.