From c47756a27d93dc6914402021a297c2144bd05263 Mon Sep 17 00:00:00 2001
From: Beast
Date: Wed, 21 Jan 2026 16:40:59 +0800
Subject: [PATCH 1/7] feat: add errors event api
---
src/api/errors.tsx | 164 ++++++++++++++++++++++++++++
src/api/index.tsx | 2 +
src/constants/query-sorts/errors.ts | 41 +++++++
src/constants/query-sorts/index.ts | 1 +
src/schemas/errors.ts | 29 +++++
src/schemas/index.ts | 1 +
6 files changed, 238 insertions(+)
create mode 100644 src/api/errors.tsx
create mode 100644 src/constants/query-sorts/errors.ts
create mode 100644 src/schemas/errors.ts
diff --git a/src/api/errors.tsx b/src/api/errors.tsx
new file mode 100644
index 0000000..77df8d6
--- /dev/null
+++ b/src/api/errors.tsx
@@ -0,0 +1,164 @@
+import type { QueryHookOptions } from '@apollo/client';
+import { gql, useQuery } from '@apollo/client';
+import { endOfToday } from 'date-fns/endOfToday';
+import { startOfToday } from 'date-fns/startOfToday';
+
+import type { ErrorEventWhereInput } from '@/__generated__/graphql';
+import { QUERY_DEFAULT_LIMIT } from '@/constants/query-default-limit';
+import { QUERY_RECENT_LIMIT } from '@/constants/query-recent-limit';
+import type { ErrorEventSorts } from '@/constants/query-sorts';
+import { ERROR_EVENT_SORTS } from '@/constants/query-sorts';
+import type {
+ ErrorEventListResponse,
+ ErrorEventResponse,
+ ErrorEventsStatsResponse,
+ RecentErrorEventsResponse
+} from '@/schemas';
+import type { PaginatedQueryVariables } from '@/types/query';
+
+export const errors = {
+ useGetAll: (
+ config?: QueryHookOptions<
+ ErrorEventListResponse,
+ PaginatedQueryVariables
+ >
+ ) => {
+ const GET_ERROR_EVENTS = gql`
+ query GetErrorEvents(
+ $limit: Int
+ $offset: Int
+ $orderBy: [ErrorEventOrderByInput!]
+ $where: ErrorEventWhereInput
+ ) {
+ errorEvents(
+ limit: $limit
+ offset: $offset
+ orderBy: $orderBy
+ where: $where
+ ) {
+ errorDocs
+ errorModule
+ errorName
+ errorType
+ extrinsicHash
+ id
+ timestamp
+ block {
+ height
+ }
+ }
+ meta: errorEventsConnection(orderBy: id_ASC, where: $where) {
+ totalCount
+ }
+ }
+ `;
+
+ return useQuery<
+ ErrorEventListResponse,
+ PaginatedQueryVariables
+ >(GET_ERROR_EVENTS, {
+ ...config,
+ variables: {
+ orderBy: config?.variables?.orderBy ?? ERROR_EVENT_SORTS.timestamp.DESC,
+ limit: config?.variables?.limit ?? QUERY_DEFAULT_LIMIT,
+ offset: config?.variables?.offset ?? 0,
+ where: config?.variables?.where
+ }
+ });
+ },
+ useGetRecent: (
+ config?: Omit, 'variables'>
+ ) => {
+ const GET_RECENT_ERROR_EVENTS = gql`
+ query GetRecentErrorEvents(
+ $limit: Int
+ $offset: Int
+ $orderBy: [ErrorEventOrderByInput!]
+ $where: ErrorEventWhereInput
+ ) {
+ errorEvents(
+ limit: $limit
+ offset: $offset
+ orderBy: $orderBy
+ where: $where
+ ) {
+ errorDocs
+ errorModule
+ errorName
+ errorType
+ extrinsicHash
+ id
+ timestamp
+ block {
+ height
+ }
+ }
+ }
+ `;
+
+ return useQuery<
+ RecentErrorEventsResponse,
+ PaginatedQueryVariables
+ >(GET_RECENT_ERROR_EVENTS, {
+ ...config,
+ variables: {
+ orderBy: ERROR_EVENT_SORTS.timestamp.DESC,
+ limit: QUERY_RECENT_LIMIT
+ }
+ });
+ },
+ useGetStats: (
+ config?: Omit, 'variables'>
+ ) => {
+ const startDate = startOfToday().toISOString();
+ const endDate = endOfToday().toISOString();
+
+ const GET_ERROR_EVENTS_STATS = gql`
+ query GetErrorEventsStats($startDate: DateTime!, $endDate: DateTime!) {
+ last24Hour: errorEventsConnection(
+ orderBy: id_ASC
+ where: { timestamp_gte: $startDate, timestamp_lte: $endDate }
+ ) {
+ totalCount
+ }
+ allTime: errorEventsConnection(orderBy: id_ASC) {
+ totalCount
+ }
+ }
+ `;
+
+ return useQuery(GET_ERROR_EVENTS_STATS, {
+ ...config,
+ variables: {
+ startDate,
+ endDate
+ }
+ });
+ },
+ getById: () => {
+ const GET_ERROR_EVENT_BY_ID = gql`
+ query GetErrorEventById($id: String!) {
+ errorEvent: errorEventById(id: $id) {
+ errorDocs
+ errorModule
+ errorName
+ errorType
+ extrinsicHash
+ id
+ timestamp
+ block {
+ height
+ }
+ }
+ }
+ `;
+
+ return {
+ useQuery: (id: string, config?: QueryHookOptions) =>
+ useQuery(GET_ERROR_EVENT_BY_ID, {
+ ...config,
+ variables: { id }
+ })
+ };
+ }
+};
diff --git a/src/api/index.tsx b/src/api/index.tsx
index c5d78a8..059ed5a 100644
--- a/src/api/index.tsx
+++ b/src/api/index.tsx
@@ -3,6 +3,7 @@ import { useFetchClient } from '@/hooks/useFetchClient';
import { accounts } from './accounts';
import { blocks } from './blocks';
import { chainStatus } from './chain-status';
+import { errors } from './errors';
import { highSecuritySets } from './high-security-sets';
import { minerLeaderboard } from './miner-leaderboard';
import { minerRewards } from './miner-rewards';
@@ -16,6 +17,7 @@ const useApiClient = () => {
const api = {
accounts,
chainStatus,
+ errors,
transactions,
reversibleTransactions,
search: search(fetcher),
diff --git a/src/constants/query-sorts/errors.ts b/src/constants/query-sorts/errors.ts
new file mode 100644
index 0000000..76f5cdf
--- /dev/null
+++ b/src/constants/query-sorts/errors.ts
@@ -0,0 +1,41 @@
+export const ERROR_EVENT_SORTS = {
+ id: {
+ ASC: 'id_ASC',
+ DESC: 'id_DESC'
+ },
+ timestamp: {
+ ASC: 'timestamp_ASC',
+ DESC: 'timestamp_DESC'
+ },
+ errorType: {
+ ASC: 'errorType_ASC',
+ DESC: 'errorType_DESC'
+ },
+ errorModule: {
+ ASC: 'errorModule_ASC',
+ DESC: 'errorModule_DESC'
+ },
+ errorName: {
+ ASC: 'errorName_ASC',
+ DESC: 'errorName_DESC'
+ },
+ extrinsicHash: {
+ ASC: 'extrinsicHash_ASC',
+ DESC: 'extrinsicHash_DESC'
+ },
+ blockHeight: {
+ ASC: 'block_height_ASC',
+ DESC: 'block_height_DESC'
+ }
+} as const;
+
+export const ERROR_EVENT_SORTS_LITERALS = Object.values(
+ ERROR_EVENT_SORTS
+).flatMap((sort) => Object.values(sort));
+
+export const ERROR_EVENT_SORTS_KEY = Object.keys(ERROR_EVENT_SORTS);
+
+// Adding null, because we can sort by null to indicate no sorting
+export type ErrorEventSorts =
+ | (typeof ERROR_EVENT_SORTS_LITERALS)[number]
+ | null;
diff --git a/src/constants/query-sorts/index.ts b/src/constants/query-sorts/index.ts
index 20ff909..c356fe1 100644
--- a/src/constants/query-sorts/index.ts
+++ b/src/constants/query-sorts/index.ts
@@ -1,5 +1,6 @@
export * from './accounts';
export * from './blocks';
+export * from './errors';
export * from './high-security-sets';
export * from './miner-rewards';
export * from './reversible-transactions';
diff --git a/src/schemas/errors.ts b/src/schemas/errors.ts
new file mode 100644
index 0000000..b2abe6e
--- /dev/null
+++ b/src/schemas/errors.ts
@@ -0,0 +1,29 @@
+import type * as gql from '../__generated__/graphql';
+
+export interface ErrorEvent extends Omit {
+ block: Pick;
+}
+
+export interface ErrorEventResponse {
+ errorEvent?: ErrorEvent | null;
+}
+
+export interface ErrorEventListResponse {
+ errorEvents: ErrorEvent[];
+ meta: {
+ totalCount: number;
+ };
+}
+
+export interface RecentErrorEventsResponse {
+ errorEvents: ErrorEvent[];
+}
+
+export interface ErrorEventsStatsResponse {
+ allTime: {
+ totalCount: number;
+ };
+ last24Hour: {
+ totalCount: number;
+ };
+}
diff --git a/src/schemas/index.ts b/src/schemas/index.ts
index acb05d8..c305e67 100644
--- a/src/schemas/index.ts
+++ b/src/schemas/index.ts
@@ -1,6 +1,7 @@
export * from './account';
export * from './blocks';
export * from './chain-status';
+export * from './errors';
export * from './high-security-set';
export * from './miner-leaderboard';
export * from './miner-reward';
From ce0e49242e992e00f76335b0e43847b90cc90c38 Mon Sep 17 00:00:00 2001
From: Beast
Date: Wed, 21 Jan 2026 16:52:12 +0800
Subject: [PATCH 2/7] feat: add error events listing
---
.../table-columns/ERROR_EVENT_COLUMNS.tsx | 56 +++++++
.../ErrorEventsHeading.tsx | 25 ++++
.../error-events-stats/ErrorEventsStats.tsx | 54 +++++++
.../error-events-table/ErrorEventsTable.tsx | 20 +++
.../error-events-table/hook.tsx | 141 ++++++++++++++++++
src/config/site-navigations.ts | 5 +
src/routes/errors/index.tsx | 27 ++++
7 files changed, 328 insertions(+)
create mode 100644 src/components/common/table-columns/ERROR_EVENT_COLUMNS.tsx
create mode 100644 src/components/features/error-events-listing/error-events-heading/ErrorEventsHeading.tsx
create mode 100644 src/components/features/error-events-listing/error-events-stats/ErrorEventsStats.tsx
create mode 100644 src/components/features/error-events-listing/error-events-table/ErrorEventsTable.tsx
create mode 100644 src/components/features/error-events-listing/error-events-table/hook.tsx
create mode 100644 src/routes/errors/index.tsx
diff --git a/src/components/common/table-columns/ERROR_EVENT_COLUMNS.tsx b/src/components/common/table-columns/ERROR_EVENT_COLUMNS.tsx
new file mode 100644
index 0000000..830f26e
--- /dev/null
+++ b/src/components/common/table-columns/ERROR_EVENT_COLUMNS.tsx
@@ -0,0 +1,56 @@
+import { createColumnHelper } from '@tanstack/react-table';
+
+import { LinkWithCopy } from '@/components/ui/composites/link-with-copy/LinkWithCopy';
+import { TimestampDisplay } from '@/components/ui/timestamp-display';
+import { RESOURCES } from '@/constants/resources';
+import type { ErrorEvent } from '@/schemas';
+import { formatTxAddress } from '@/utils/formatter';
+
+const columnHelper = createColumnHelper();
+
+export const ERROR_EVENT_COLUMNS = [
+ columnHelper.accessor('extrinsicHash', {
+ id: 'extrinsicHash',
+ header: 'Extrinsic Hash',
+ cell: (props) =>
+ props.getValue() ? (
+
+ ) : (
+ 'Is not available'
+ ),
+ enableSorting: false
+ }),
+ columnHelper.accessor('block.height', {
+ id: 'block_height',
+ header: 'Block',
+ cell: (props) => (
+
+ ),
+ enableSorting: true
+ }),
+ columnHelper.accessor('timestamp', {
+ id: 'timestamp',
+ header: 'Timestamp',
+ cell: (props) => ,
+ enableSorting: true
+ }),
+ columnHelper.accessor('errorType', {
+ id: 'errorType',
+ header: 'Type',
+ cell: (props) => props.getValue(),
+ enableSorting: true
+ }),
+ columnHelper.accessor('errorName', {
+ id: 'errorName',
+ header: 'Name',
+ cell: (props) => props.getValue() ?? '-',
+ enableSorting: true
+ })
+];
diff --git a/src/components/features/error-events-listing/error-events-heading/ErrorEventsHeading.tsx b/src/components/features/error-events-listing/error-events-heading/ErrorEventsHeading.tsx
new file mode 100644
index 0000000..2b948af
--- /dev/null
+++ b/src/components/features/error-events-listing/error-events-heading/ErrorEventsHeading.tsx
@@ -0,0 +1,25 @@
+import { Link, useSearch } from '@tanstack/react-router';
+import * as React from 'react';
+
+export interface ErrorEventsHeadingProps {}
+
+export const ErrorEventsHeading: React.FC = () => {
+ const { block } = useSearch({
+ strict: false
+ }) as any;
+
+ return (
+
+
Error Events
+
+ {block && (
+
+ In block
+
+ {block}
+
+
+ )}
+
+ );
+};
diff --git a/src/components/features/error-events-listing/error-events-stats/ErrorEventsStats.tsx b/src/components/features/error-events-listing/error-events-stats/ErrorEventsStats.tsx
new file mode 100644
index 0000000..dd7281c
--- /dev/null
+++ b/src/components/features/error-events-listing/error-events-stats/ErrorEventsStats.tsx
@@ -0,0 +1,54 @@
+import { useSearch } from '@tanstack/react-router';
+import React from 'react';
+
+import useApiClient from '@/api';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Skeleton } from '@/components/ui/skeleton';
+import { DATA_POOL_INTERVAL } from '@/constants/data-pool-interval';
+
+export interface ErrorEventsStatsProps {}
+
+export const ErrorEventsStats: React.FC = () => {
+ const api = useApiClient();
+ const { block } = useSearch({
+ strict: false
+ }) as any;
+
+ if (block) return null;
+
+ const { loading, data, error } = api.errors.useGetStats({
+ pollInterval: DATA_POOL_INTERVAL
+ });
+
+ const success = !loading && !error;
+
+ return (
+
+
+
+
+ Total Error Events
+
+
+
+ {success && {data?.allTime.totalCount}
}
+ {loading && }
+ {error && Error: {error.message}
}
+
+
+
+
+
+
+ Recent Error Events (24H)
+
+
+
+ {success && {data?.last24Hour.totalCount}
}
+ {loading && }
+ {error && Error: {error.message}
}
+
+
+
+ );
+};
diff --git a/src/components/features/error-events-listing/error-events-table/ErrorEventsTable.tsx b/src/components/features/error-events-listing/error-events-table/ErrorEventsTable.tsx
new file mode 100644
index 0000000..6c39f51
--- /dev/null
+++ b/src/components/features/error-events-listing/error-events-table/ErrorEventsTable.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+
+import { DataTable } from '@/components/ui/composites/data-table/DataTable';
+
+import { useErrorEventsTable } from './hook';
+
+export const ErrorEventsTable = () => {
+ const { getStatus, table, error } = useErrorEventsTable();
+
+ return (
+ Error: {error && error.message}
+ }}
+ withControls
+ />
+ );
+};
diff --git a/src/components/features/error-events-listing/error-events-table/hook.tsx b/src/components/features/error-events-listing/error-events-table/hook.tsx
new file mode 100644
index 0000000..f28f75e
--- /dev/null
+++ b/src/components/features/error-events-listing/error-events-table/hook.tsx
@@ -0,0 +1,141 @@
+import { useSearch } from '@tanstack/react-router';
+import type {
+ OnChangeFn,
+ PaginationState,
+ SortingState
+} from '@tanstack/react-table';
+import { getCoreRowModel, useReactTable } from '@tanstack/react-table';
+import { parseAsInteger, parseAsStringLiteral, useQueryState } from 'nuqs';
+import { useEffect, useMemo, useState } from 'react';
+
+import useApiClient from '@/api';
+import { ERROR_EVENT_COLUMNS } from '@/components/common/table-columns/ERROR_EVENT_COLUMNS';
+import { DATA_POOL_INTERVAL } from '@/constants/data-pool-interval';
+import { QUERY_DEFAULT_LIMIT } from '@/constants/query-default-limit';
+import type { ErrorEventSorts } from '@/constants/query-sorts';
+import { ERROR_EVENT_SORTS_LITERALS } from '@/constants/query-sorts';
+import type { ErrorEvent } from '@/schemas';
+import { transformSortLiteral } from '@/utils/transform-sort';
+
+export const useErrorEventsTable = () => {
+ const api = useApiClient();
+ const { block } = useSearch({
+ strict: false
+ }) as any;
+
+ const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
+ const [limit, setLimit] = useQueryState(
+ 'limit',
+ parseAsInteger.withDefault(QUERY_DEFAULT_LIMIT)
+ );
+ const [sortBy, setSortBy] = useQueryState(
+ 'sortBy',
+ parseAsStringLiteral(ERROR_EVENT_SORTS_LITERALS)
+ );
+
+ const currentPageIndex = page - 1;
+
+ const sortingValue: SortingState = transformSortLiteral(sortBy);
+ const paginationValue: PaginationState = {
+ pageSize: limit,
+ pageIndex: currentPageIndex
+ };
+
+ const handleChangeSorting: OnChangeFn = (sorting) => {
+ if (typeof sorting === 'function') {
+ const newValue = sorting(sortingValue);
+
+ if (newValue[0]?.id) {
+ const key = newValue[0].id;
+ const order = newValue[0].desc ? 'DESC' : 'ASC';
+
+ const newSortBy = `${key}_${order}` as ErrorEventSorts;
+
+ setSortBy(newSortBy);
+ } else {
+ setSortBy(null);
+ }
+ } else if (sorting[0]?.id) {
+ const key = sorting[0].id;
+ const order = sorting[0].desc ? 'DESC' : 'ASC';
+
+ const newSortBy = `${key}_${order}` as ErrorEventSorts;
+
+ setSortBy(newSortBy);
+ }
+ };
+
+ const handleChangePagination: OnChangeFn = (pagination) => {
+ if (typeof pagination === 'function') {
+ const newPagination = pagination(paginationValue);
+
+ setPage(newPagination.pageIndex + 1);
+ setLimit(newPagination.pageSize);
+ } else {
+ setPage(pagination.pageIndex + 1);
+ setLimit(pagination.pageSize);
+ }
+ };
+
+ const {
+ loading,
+ data,
+ error: fetchError
+ } = api.errors.useGetAll({
+ pollInterval: DATA_POOL_INTERVAL,
+ variables: {
+ orderBy: sortBy,
+ limit,
+ offset: currentPageIndex * limit,
+ ...(block && {
+ where: {
+ block: { height_eq: Number(block) }
+ }
+ })
+ }
+ });
+
+ const errorEventColumns = useMemo(() => ERROR_EVENT_COLUMNS, []);
+ const [rowCount, setRowCount] = useState(data?.meta.totalCount ?? 0);
+
+ const table = useReactTable({
+ data: data?.errorEvents ?? [],
+ columns: errorEventColumns,
+ getCoreRowModel: getCoreRowModel(),
+ state: {
+ sorting: sortingValue,
+ pagination: paginationValue
+ },
+ rowCount,
+ onSortingChange: handleChangeSorting,
+ onPaginationChange: handleChangePagination,
+ manualSorting: true,
+ manualPagination: true
+ });
+
+ const success = !loading && !fetchError;
+ const error = !loading && fetchError;
+
+ const getStatus = () => {
+ switch (true) {
+ case success:
+ return 'success';
+ case !!error:
+ return 'error';
+ case !!loading:
+ return 'loading';
+ default:
+ return 'idle';
+ }
+ };
+
+ useEffect(() => {
+ if (!loading && data?.meta.totalCount) setRowCount(data.meta.totalCount);
+ }, [loading, data?.meta.totalCount]);
+
+ return {
+ table,
+ getStatus,
+ error
+ };
+};
diff --git a/src/config/site-navigations.ts b/src/config/site-navigations.ts
index c43d95c..017d0bb 100644
--- a/src/config/site-navigations.ts
+++ b/src/config/site-navigations.ts
@@ -60,5 +60,10 @@ export const SITE_NAVIGATIONS: (SiteNavigation | ParentNavigation)[] = [
label: 'Blocks',
path: '/blocks',
longLabel: 'Blocks'
+ },
+ {
+ label: 'Errors',
+ path: '/errors',
+ longLabel: 'Error Events'
}
] as const;
diff --git a/src/routes/errors/index.tsx b/src/routes/errors/index.tsx
new file mode 100644
index 0000000..ee573f4
--- /dev/null
+++ b/src/routes/errors/index.tsx
@@ -0,0 +1,27 @@
+import { createFileRoute } from '@tanstack/react-router';
+
+import { ErrorEventsHeading } from '@/components/features/error-events-listing/error-events-heading/ErrorEventsHeading';
+import { ErrorEventsStats } from '@/components/features/error-events-listing/error-events-stats/ErrorEventsStats';
+import { ErrorEventsTable } from '@/components/features/error-events-listing/error-events-table/ErrorEventsTable';
+import { ContentContainer } from '@/components/ui/content-container';
+import { SectionContainer } from '@/components/ui/section-container';
+
+export const Route = createFileRoute('/errors/')({
+ component: ErrorEvents
+});
+
+function ErrorEvents() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ErrorEvents;
From 63a1699e7e053eb37b8d6c4c3c79700532130c42 Mon Sep 17 00:00:00 2001
From: Beast
Date: Wed, 21 Jan 2026 17:53:13 +0800
Subject: [PATCH 3/7] feat: add error event details
---
.../ErrorEventInformation.tsx | 108 ++++++++++++++++++
src/constants/resources.ts | 3 +-
src/routeTree.gen.ts | 42 +++++++
src/routes/errors/$id.tsx | 26 +++++
4 files changed, 178 insertions(+), 1 deletion(-)
create mode 100644 src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx
create mode 100644 src/routes/errors/$id.tsx
diff --git a/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx b/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx
new file mode 100644
index 0000000..99affd6
--- /dev/null
+++ b/src/components/features/error-event-details/error-event-information/ErrorEventInformation.tsx
@@ -0,0 +1,108 @@
+import { notFound } from '@tanstack/react-router';
+import * as React from 'react';
+
+import useApiClient from '@/api';
+import { DataList } from '@/components/ui/composites/data-list/DataList';
+import { LinkWithCopy } from '@/components/ui/composites/link-with-copy/LinkWithCopy';
+import { TextWithCopy } from '@/components/ui/composites/text-with-copy/TextWithCopy';
+import { RESOURCES } from '@/constants/resources';
+import type { ErrorEvent } from '@/schemas';
+import { formatTimestamp } from '@/utils/formatter';
+
+export interface ErrorEventInformationProps {
+ id: string;
+}
+
+export const ErrorEventInformation: React.FC = ({
+ id
+}) => {
+ const api = useApiClient();
+ const { data, loading } = api.errors.getById().useQuery(id);
+
+ if (!loading && (!data || !data.errorEvent)) throw notFound();
+
+ const event = data?.errorEvent ?? undefined;
+
+ const information: Partial[] = [
+ {
+ id: event?.id,
+ timestamp: event?.timestamp,
+ block: event?.block,
+ extrinsicHash: event?.extrinsicHash,
+ errorType: event?.errorType,
+ errorModule: event?.errorModule,
+ errorName: event?.errorName,
+ errorDocs: event?.errorDocs
+ }
+ ];
+
+ return (
+