diff --git a/.changeset/calm-jobs-lay.md b/.changeset/calm-jobs-lay.md new file mode 100644 index 0000000000..d4394a3dc4 --- /dev/null +++ b/.changeset/calm-jobs-lay.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-sdk": minor +--- + +Added indexing status based functions for checking Omnigraph API and Subgraph API availability. diff --git a/.changeset/four-cats-sink.md b/.changeset/four-cats-sink.md new file mode 100644 index 0000000000..29ecd891de --- /dev/null +++ b/.changeset/four-cats-sink.md @@ -0,0 +1,5 @@ +--- +"ensadmin": minor +--- + +Added indexing status based guard for Omnigraph API and Subgraph API views. diff --git a/.changeset/loose-pets-vanish.md b/.changeset/loose-pets-vanish.md new file mode 100644 index 0000000000..8d9923c75b --- /dev/null +++ b/.changeset/loose-pets-vanish.md @@ -0,0 +1,5 @@ +--- +"ensapi": minor +--- + +Added indexing status based guard for Omnigraph API and Subgraph API routes. diff --git a/apps/ensadmin/src/hooks/active/use-ensadmin-features.tsx b/apps/ensadmin/src/hooks/active/use-ensadmin-features.tsx index c56dff7be5..248f4237ea 100644 --- a/apps/ensadmin/src/hooks/active/use-ensadmin-features.tsx +++ b/apps/ensadmin/src/hooks/active/use-ensadmin-features.tsx @@ -2,9 +2,11 @@ import { useMemo } from "react"; import { hasOmnigraphApiConfigSupport, + hasOmnigraphApiIndexingStatusSupport, hasRegistrarActionsConfigSupport, hasRegistrarActionsIndexingStatusSupport, hasSubgraphApiConfigSupport, + hasSubgraphApiIndexingStatusSupport, PrerequisiteResult, } from "@ensnode/ensnode-sdk"; @@ -86,7 +88,19 @@ export function useENSAdminFeatures(): ENSAdminFeatures { if (indexingStatusQuery.status === "pending") return CONNECTING_STATUS; const { ensIndexer: ensIndexerPublicConfig } = indexingStatusQuery.data.stackInfo; - return prerequisiteResultToFeatureStatus(hasSubgraphApiConfigSupport(ensIndexerPublicConfig)); + const configSupportResult = hasSubgraphApiConfigSupport(ensIndexerPublicConfig); + if (!configSupportResult.supported) + return prerequisiteResultToFeatureStatus(configSupportResult); + + const { realtimeProjection } = indexingStatusQuery.data; + const { omnichainSnapshot } = realtimeProjection.snapshot; + + const indexingStatusSupportResult = hasSubgraphApiIndexingStatusSupport( + omnichainSnapshot.omnichainStatus, + ); + if (!indexingStatusSupportResult.supported) + return { type: "not-ready", reason: indexingStatusSupportResult.reason }; + return { type: "supported" }; }, [indexingStatusQuery]); const omnigraph: FeatureStatus = useMemo(() => { @@ -94,7 +108,19 @@ export function useENSAdminFeatures(): ENSAdminFeatures { if (indexingStatusQuery.status === "pending") return CONNECTING_STATUS; const { ensIndexer: ensIndexerPublicConfig } = indexingStatusQuery.data.stackInfo; - return prerequisiteResultToFeatureStatus(hasOmnigraphApiConfigSupport(ensIndexerPublicConfig)); + const configSupportResult = hasOmnigraphApiConfigSupport(ensIndexerPublicConfig); + if (!configSupportResult.supported) + return prerequisiteResultToFeatureStatus(configSupportResult); + + const { realtimeProjection } = indexingStatusQuery.data; + const { omnichainSnapshot } = realtimeProjection.snapshot; + + const indexingStatusSupportResult = hasOmnigraphApiIndexingStatusSupport( + omnichainSnapshot.omnichainStatus, + ); + if (!indexingStatusSupportResult.supported) + return { type: "not-ready", reason: indexingStatusSupportResult.reason }; + return { type: "supported" }; }, [indexingStatusQuery]); const restApi: FeatureStatus = useMemo(() => { diff --git a/apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts b/apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts index 31fbf3d292..ec2d40362c 100644 --- a/apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts +++ b/apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts @@ -1,16 +1,34 @@ import config from "@/config"; -import { hasOmnigraphApiConfigSupport } from "@ensnode/ensnode-sdk"; +import { + hasOmnigraphApiConfigSupport, + hasOmnigraphApiIndexingStatusSupport, +} from "@ensnode/ensnode-sdk"; import { createApp } from "@/lib/hono-factory"; +import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; -const app = createApp(); +const app = createApp({ middlewares: [indexingStatusMiddleware] }); -// 503 if prerequisites not met app.use(async (c, next) => { - const prerequisite = hasOmnigraphApiConfigSupport(config.ensIndexerPublicConfig); - if (!prerequisite.supported) { - return c.text(`Service Unavailable: ${prerequisite.reason}`, 503); + const configPrerequisite = hasOmnigraphApiConfigSupport(config.ensIndexerPublicConfig); + // 503 if Omnigraph API is not available due to config prerequisites not met + if (!configPrerequisite.supported) { + return c.text(`Service Unavailable: ${configPrerequisite.reason}`, 503); + } + + // 503 if indexing status snapshot is not available yet + if (c.var.indexingStatus instanceof Error) { + return c.text(`Service Unavailable: Indexing Status Snapshot is not available yet`, 503); + } + + // 503 if omnigraph API not available due to indexing status prerequisites not met + const indexingStatusPrerequisite = hasOmnigraphApiIndexingStatusSupport( + c.var.indexingStatus.snapshot.omnichainSnapshot.omnichainStatus, + ); + + if (!indexingStatusPrerequisite.supported) { + return c.text(`Service Unavailable: ${indexingStatusPrerequisite.reason}`, 503); } await next(); diff --git a/apps/ensapi/src/handlers/subgraph/subgraph-api.ts b/apps/ensapi/src/handlers/subgraph/subgraph-api.ts index b0b01929c1..97fa5ed225 100644 --- a/apps/ensapi/src/handlers/subgraph/subgraph-api.ts +++ b/apps/ensapi/src/handlers/subgraph/subgraph-api.ts @@ -8,7 +8,10 @@ import { createDocumentationMiddleware } from "ponder-enrich-gql-docs-middleware // Once the lazy proxy implemented for `ensIndexerSchema` export is improved // to support Drizzle ORM in `ponder-subgraph` package. import * as ensIndexerSchema from "@ensnode/ensdb-sdk/ensindexer-abstract"; -import { hasSubgraphApiConfigSupport } from "@ensnode/ensnode-sdk"; +import { + hasSubgraphApiConfigSupport, + hasSubgraphApiIndexingStatusSupport, +} from "@ensnode/ensnode-sdk"; import { subgraphGraphQLMiddleware } from "@ensnode/ponder-subgraph"; import { createApp } from "@/lib/hono-factory"; @@ -26,21 +29,32 @@ const MAX_REALTIME_DISTANCE_TO_RESOLVE: Duration = 10 * 60; // 10 minutes in sec // generate a subgraph-specific subset of the schema const subgraphSchema = filterSchemaByPrefix("subgraph_", ensIndexerSchema); -const app = createApp(); +const app = createApp({ middlewares: [indexingStatusMiddleware] }); -// 503 if subgraph plugin not available app.use(async (c, next) => { - const prerequisite = hasSubgraphApiConfigSupport(config.ensIndexerPublicConfig); - if (!prerequisite.supported) { - return c.text(`Service Unavailable: ${prerequisite.reason}`, 503); + const configPrerequisite = hasSubgraphApiConfigSupport(config.ensIndexerPublicConfig); + // 503 if Subgraph API is not available due to config prerequisites not met + if (!configPrerequisite.supported) { + return c.text(`Service Unavailable: ${configPrerequisite.reason}`, 503); + } + + // 503 if indexing status snapshot is not available yet + if (c.var.indexingStatus instanceof Error) { + return c.text(`Service Unavailable: Indexing Status Snapshot is not available yet`, 503); + } + + // 503 if Subgraph API is not available due to indexing status prerequisites not met + const indexingStatusPrerequisite = hasSubgraphApiIndexingStatusSupport( + c.var.indexingStatus.snapshot.omnichainSnapshot.omnichainStatus, + ); + + if (!indexingStatusPrerequisite.supported) { + return c.text(`Service Unavailable: ${indexingStatusPrerequisite.reason}`, 503); } await next(); }); -// inject c.var.indexingStatus -app.use(indexingStatusMiddleware); - // inject c.var.isRealtime derived from MAX_REALTIME_DISTANCE_TO_RESOLVE app.use(makeIsRealtimeMiddleware("subgraph-api", MAX_REALTIME_DISTANCE_TO_RESOLVE)); diff --git a/packages/ensnode-sdk/src/ensnode/api/prerequisites.ts b/packages/ensnode-sdk/src/ensnode/api/prerequisites.ts new file mode 100644 index 0000000000..b4f71793a2 --- /dev/null +++ b/packages/ensnode-sdk/src/ensnode/api/prerequisites.ts @@ -0,0 +1,24 @@ +import { type OmnichainIndexingStatusId, OmnichainIndexingStatusIds } from "../../indexing-status"; +import type { PrerequisiteResult } from "../../shared/prerequisites"; + +/** + * Check if provided OmnichainIndexingStatusId indicates that the backfill is complete. + * + * This is a prerequisite for all APIs that rely on indexed data. We need to ensure that + * the backfill is complete to guarantee that the necessary data is completely indexed + * and available for queries. + */ +export function hasBackfillCompleted( + indexingStatus: OmnichainIndexingStatusId, +): PrerequisiteResult { + const supported = + indexingStatus === OmnichainIndexingStatusIds.Completed || + indexingStatus === OmnichainIndexingStatusIds.Following; + + if (supported) return { supported }; + + return { + supported: false, + reason: `The connected ENSNode's Indexing Status must be "${OmnichainIndexingStatusIds.Completed}" or "${OmnichainIndexingStatusIds.Following}". Currently, it is "${indexingStatus}".`, + }; +} diff --git a/packages/ensnode-sdk/src/omnigraph-api/prerequisites.ts b/packages/ensnode-sdk/src/omnigraph-api/prerequisites.ts index 10f466601e..d3b3c3dd14 100644 --- a/packages/ensnode-sdk/src/omnigraph-api/prerequisites.ts +++ b/packages/ensnode-sdk/src/omnigraph-api/prerequisites.ts @@ -1,8 +1,10 @@ import { type EnsIndexerPublicConfig, PluginName } from "../ensindexer/config/types"; +import { hasBackfillCompleted } from "../ensnode/api/prerequisites"; +import type { OmnichainIndexingStatusId } from "../indexing-status"; import type { PrerequisiteResult } from "../shared/prerequisites"; /** - * Check if provided EnsIndexerPublicConfig supports the ENSNode Omnigraph API. + * Check if provided EnsIndexerPublicConfig supports the Omnigraph API. */ export function hasOmnigraphApiConfigSupport(config: EnsIndexerPublicConfig): PrerequisiteResult { const supported = config.plugins.includes(PluginName.ENSv2); @@ -13,3 +15,12 @@ export function hasOmnigraphApiConfigSupport(config: EnsIndexerPublicConfig): Pr reason: `The connected ENSNode's Config must have the '${PluginName.ENSv2}' plugin enabled.`, }; } + +/** + * Check if provided OmnichainIndexingStatusId supports the Omnigraph API. + */ +export function hasOmnigraphApiIndexingStatusSupport( + indexingStatus: OmnichainIndexingStatusId, +): PrerequisiteResult { + return hasBackfillCompleted(indexingStatus); +} diff --git a/packages/ensnode-sdk/src/subgraph-api/prerequisites.ts b/packages/ensnode-sdk/src/subgraph-api/prerequisites.ts index 63aa22b9c2..70107f2a97 100644 --- a/packages/ensnode-sdk/src/subgraph-api/prerequisites.ts +++ b/packages/ensnode-sdk/src/subgraph-api/prerequisites.ts @@ -1,4 +1,6 @@ import { type EnsIndexerPublicConfig, PluginName } from "../ensindexer/config/types"; +import { hasBackfillCompleted } from "../ensnode/api/prerequisites"; +import type { OmnichainIndexingStatusId } from "../indexing-status"; import type { PrerequisiteResult } from "../shared/prerequisites"; /** @@ -13,3 +15,12 @@ export function hasSubgraphApiConfigSupport(config: EnsIndexerPublicConfig): Pre reason: `The connected ENSNode's Config must have the '${PluginName.Subgraph}' plugin enabled.`, }; } + +/** + * Check if provided OmnichainIndexingStatusId supports the Subgraph API. + */ +export function hasSubgraphApiIndexingStatusSupport( + indexingStatus: OmnichainIndexingStatusId, +): PrerequisiteResult { + return hasBackfillCompleted(indexingStatus); +}