Skip to content
5 changes: 5 additions & 0 deletions .changeset/shaggy-dodos-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensapi": minor
---

Indexing Status cache only stores responses with `responseCode: IndexingStatusResponseCodes.Ok`.
10 changes: 9 additions & 1 deletion apps/ensapi/src/handlers/ensanalytics-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import { errorResponse } from "@/lib/handlers/error-response";
import { validate } from "@/lib/handlers/validate";
import { factory } from "@/lib/hono-factory";
import { islice } from "@/lib/itertools";
import logger from "@/lib/logger";
import { makeLogger } from "@/lib/logger";
import { aggregatedReferrerSnapshotCacheMiddleware } from "@/middleware/aggregated-referrer-snapshot-cache.middleware";

const app = factory.createApp();
const logger = makeLogger("ensanalytics-api");

// Apply aggregated referrer snapshot cache middleware to all routes in this handler
app.use(aggregatedReferrerSnapshotCacheMiddleware);
Expand Down Expand Up @@ -64,6 +65,13 @@ function calculateContribution(

// Get all aggregated referrers with pagination
app.get("/aggregated-referrers", validate("query", paginationQuerySchema), async (c) => {
// context must be set by the required middleware
if (c.var.aggregatedReferrerSnapshotCache === undefined) {
throw new Error(
`Invariant(ensanalytics-api): aggregatedReferrerSnapshotCacheMiddleware required`,
);
}

try {
const aggregatedReferrerSnapshotCache = c.var.aggregatedReferrerSnapshotCache;

Expand Down
26 changes: 24 additions & 2 deletions apps/ensapi/src/handlers/ensnode-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ import config from "@/config";
import {
IndexingStatusResponseCodes,
type IndexingStatusResponseError,
type IndexingStatusResponseOk,
serializeENSApiPublicConfig,
serializeIndexingStatusResponse,
} from "@ensnode/ensnode-sdk";

import { buildEnsApiPublicConfig } from "@/config/config.schema";
import { factory } from "@/lib/hono-factory";
import { makeLogger } from "@/lib/logger";

import registrarActionsApi from "./registrar-actions-api";
import resolutionApi from "./resolution-api";

const app = factory.createApp();

const logger = makeLogger("ensnode-api");

// include ENSApi Public Config endpoint
app.get("/config", async (c) => {
const ensApiPublicConfig = buildEnsApiPublicConfig(config);
Expand All @@ -23,8 +27,20 @@ app.get("/config", async (c) => {

// include ENSIndexer Indexing Status endpoint
app.get("/indexing-status", async (c) => {
// generic error
// context must be set by the required middleware
if (c.var.indexingStatus === undefined) {
throw new Error(`Invariant(ensnode-api): indexingStatusMiddleware required`);
}

if (c.var.indexingStatus.isRejected) {
// no indexing status available in context
logger.error(
{
error: c.var.indexingStatus.reason,
},
"Indexing status requested but is not available in context.",
);

return c.json(
serializeIndexingStatusResponse({
responseCode: IndexingStatusResponseCodes.Error,
Expand All @@ -33,7 +49,13 @@ app.get("/indexing-status", async (c) => {
);
}

return c.json(serializeIndexingStatusResponse(c.var.indexingStatus.value));
// return successful response using the indexing status projection from the context
return c.json(
serializeIndexingStatusResponse({
responseCode: IndexingStatusResponseCodes.Ok,
realtimeProjection: c.var.indexingStatus.value,
} satisfies IndexingStatusResponseOk),
);
});

// Registrar Actions API
Expand Down
6 changes: 3 additions & 3 deletions apps/ensapi/src/handlers/registrar-actions-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ import { params } from "@/lib/handlers/params.schema";
import { validate } from "@/lib/handlers/validate";
import { factory } from "@/lib/hono-factory";
import { makeLogger } from "@/lib/logger";
import { requireRegistrarActionsPluginMiddleware } from "@/lib/middleware/require-registrar-actions-plugins..middleware";
import { findRegistrarActions } from "@/lib/registrar-actions/find-registrar-actions";
import { registrarActionsApiMiddleware } from "@/middleware/registrar-actions.middleware";

const app = factory.createApp();

const logger = makeLogger("registrar-actions");
const logger = makeLogger("registrar-actions-api");

// Middleware managing access to Registrar Actions API routes.
// It makes the routes available if all prerequisites are met.
app.use(requireRegistrarActionsPluginMiddleware());
app.use(registrarActionsApiMiddleware);

const RESPONSE_ITEMS_PER_PAGE_DEFAULT = 25;
const RESPONSE_ITEMS_PER_PAGE_MAX = 100;
Expand Down
15 changes: 15 additions & 0 deletions apps/ensapi/src/handlers/resolution-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ app.get(
}),
),
async (c) => {
// context must be set by the required middleware
if (c.var.canAccelerate === undefined) {
throw new Error(`Invariant(resolution-api): canAccelerateMiddleware required`);
}

const { name } = c.req.valid("param");
const { selection, trace: showTrace, accelerate } = c.req.valid("query");
const canAccelerate = c.var.canAccelerate;
Expand Down Expand Up @@ -103,6 +108,11 @@ app.get(
}),
),
async (c) => {
// context must be set by the required middleware
if (c.var.canAccelerate === undefined) {
throw new Error(`Invariant(resolution-api): canAccelerateMiddleware required`);
}

const { address, chainId } = c.req.valid("param");
const { trace: showTrace, accelerate } = c.req.valid("query");
const canAccelerate = c.var.canAccelerate;
Expand Down Expand Up @@ -144,6 +154,11 @@ app.get(
}),
),
async (c) => {
// context must be set by the required middleware
if (c.var.canAccelerate === undefined) {
throw new Error(`Invariant(resolution-api): canAccelerateMiddleware required`);
}

const { address } = c.req.valid("param");
const { chainIds, trace: showTrace, accelerate } = c.req.valid("query");
const canAccelerate = c.var.canAccelerate;
Expand Down
20 changes: 12 additions & 8 deletions apps/ensapi/src/lib/hono-factory.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { createFactory } from "hono/factory";

import type { AggregatedReferrerSnapshotCacheVariables } from "@/middleware/aggregated-referrer-snapshot-cache.middleware";
import type { CanAccelerateVariables } from "@/middleware/can-accelerate.middleware";
import type { IndexingStatusVariables } from "@/middleware/indexing-status.middleware";
import type { IsRealtimeVariables } from "@/middleware/is-realtime.middleware";
import type { AggregatedReferrerSnapshotCacheMiddlewareVariables } from "@/middleware/aggregated-referrer-snapshot-cache.middleware";
import type { CanAccelerateMiddlewareVariables } from "@/middleware/can-accelerate.middleware";
import type { IndexingStatusMiddlewareVariables } from "@/middleware/indexing-status.middleware";
import type { IsRealtimeMiddlewareVariables } from "@/middleware/is-realtime.middleware";

type MiddlewareVariables = Partial<
IndexingStatusMiddlewareVariables &
IsRealtimeMiddlewareVariables &
CanAccelerateMiddlewareVariables &
AggregatedReferrerSnapshotCacheMiddlewareVariables
>;

export const factory = createFactory<{
Variables: IndexingStatusVariables &
IsRealtimeVariables &
CanAccelerateVariables &
AggregatedReferrerSnapshotCacheVariables;
Variables: MiddlewareVariables;
}>();
82 changes: 35 additions & 47 deletions apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,52 @@
import config from "@/config";

import {
ChainIndexingStatusIds,
getENSRootChainId,
IndexingStatusResponseCodes,
} from "@ensnode/ensnode-sdk";
import { ChainIndexingStatusIds, getENSRootChainId } from "@ensnode/ensnode-sdk";
import type { SubgraphMeta } from "@ensnode/ponder-subgraph";

import type { IndexingStatusVariables } from "@/middleware/indexing-status.middleware";
import type { IndexingStatusMiddlewareVariables } from "@/middleware/indexing-status.middleware";

/**
* Converts ENSIndexer indexing status to GraphQL subgraph metadata format.
*
* Transforms the indexing status response from ENSIndexer into the `_meta` format
* expected by legacy subgraph GraphQL APIs. Returns null if the indexing status
* indicates an error state or the root chain is not available.
* Transforms the indexing context from the indexing status middleware into
* the `_meta` format expected by legacy subgraph GraphQL APIs.
* Returns null if the indexing context indicates an error state or
* indexing status for the ENS root chain is not available.
*
* @param indexingStatus - The indexing status result from ENSIndexer
* @param indexingStatus - The indexing context from the indexing status middleware
* @returns SubgraphMeta object or null if conversion is not possible
*/
export function indexingStatusToSubgraphMeta(
indexingStatus: IndexingStatusVariables["indexingStatus"],
export function indexingContextToSubgraphMeta(
indexingStatus: IndexingStatusMiddlewareVariables["indexingStatus"],
): SubgraphMeta {
switch (indexingStatus.status) {
case "rejected": {
if (indexingStatus.isRejected) {
// indexing status middleware has never successfully fetched (and cached) an indexing status snapshot
// for the lifetime of this service instance.
return null;
}

const rootChain = indexingStatus.value.snapshot.omnichainSnapshot.chains.get(
getENSRootChainId(config.namespace),
);
if (!rootChain) return null;

switch (rootChain.chainStatus) {
case ChainIndexingStatusIds.Queued: {
return null;
}
case "fulfilled": {
switch (indexingStatus.value.responseCode) {
case IndexingStatusResponseCodes.Error: {
return null;
}
case IndexingStatusResponseCodes.Ok: {
const rootChain =
indexingStatus.value.realtimeProjection.snapshot.omnichainSnapshot.chains.get(
getENSRootChainId(config.namespace),
);
if (!rootChain) return null;

switch (rootChain.chainStatus) {
case ChainIndexingStatusIds.Queued: {
return null;
}
case ChainIndexingStatusIds.Completed:
case ChainIndexingStatusIds.Backfill:
case ChainIndexingStatusIds.Following: {
return {
deployment: config.ensIndexerPublicConfig.versionInfo.ensIndexer,
hasIndexingErrors: false,
block: {
hash: null,
parentHash: null,
number: rootChain.latestIndexedBlock.number,
timestamp: rootChain.latestIndexedBlock.timestamp,
},
};
}
}
}
}
case ChainIndexingStatusIds.Completed:
case ChainIndexingStatusIds.Backfill:
case ChainIndexingStatusIds.Following: {
return {
deployment: config.ensIndexerPublicConfig.versionInfo.ensIndexer,
hasIndexingErrors: false,
block: {
hash: null,
parentHash: null,
number: rootChain.latestIndexedBlock.number,
timestamp: rootChain.latestIndexedBlock.timestamp,
},
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,41 @@ import {

import { getAggregatedReferrerSnapshot } from "@/lib/ensanalytics/database";
import { factory } from "@/lib/hono-factory";
import logger from "@/lib/logger";
import { makeLogger } from "@/lib/logger";

const logger = makeLogger("aggregated-referrer-snapshot-cache.middleware");

const TTL: Duration = 5 * 60; // 5 minutes

export const fetcher = staleWhileRevalidate(async () => {
logger.info(
`Building aggregated referrer snapshot\n` +
` - ENS Holiday Awards start timestamp: ${config.ensHolidayAwardsStart}\n` +
` - ENS Holiday Awards end timestamp: ${config.ensHolidayAwardsEnd}`,
);
const subregistryId = getEthnamesSubregistryId(config.namespace);

try {
const result = await getAggregatedReferrerSnapshot(
config.ensHolidayAwardsStart,
config.ensHolidayAwardsEnd,
subregistryId,
export const fetcher = staleWhileRevalidate({
fn: async () => {
logger.info(
`Building aggregated referrer snapshot\n` +
` - ENS Holiday Awards start timestamp: ${config.ensHolidayAwardsStart}\n` +
` - ENS Holiday Awards end timestamp: ${config.ensHolidayAwardsEnd}`,
);
logger.info("Successfully built aggregated referrer snapshot");
return result;
} catch (error) {
logger.error({ error }, "Failed to build aggregated referrer snapshot");
throw error;
}
}, TTL);

export type AggregatedReferrerSnapshotCacheVariables = {
const subregistryId = getEthnamesSubregistryId(config.namespace);

try {
const result = await getAggregatedReferrerSnapshot(
config.ensHolidayAwardsStart,
config.ensHolidayAwardsEnd,
subregistryId,
);
logger.info(
{ grandTotalReferrals: result.referrers.size },
"Successfully built aggregated referrer snapshot",
);
return result;
} catch (error) {
logger.error({ error }, "Failed to build aggregated referrer snapshot");
throw error;
}
},
ttl: TTL,
});

export type AggregatedReferrerSnapshotCacheMiddlewareVariables = {
aggregatedReferrerSnapshotCache: Awaited<ReturnType<typeof fetcher>>;
};

Expand Down
3 changes: 2 additions & 1 deletion apps/ensapi/src/middleware/can-accelerate.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { makeLogger } from "@/lib/logger";

const logger = makeLogger("can-accelerate.middleware");

export type CanAccelerateVariables = { canAccelerate: boolean };
export type CanAccelerateMiddlewareVariables = { canAccelerate: boolean };

// TODO: expand this datamodel to include 'reasons' acceleration was disabled to drive ui

Expand All @@ -23,6 +23,7 @@ let prevCanAccelerate = false;
* resolution handlers.
*/
export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) => {
// context must be set by the required middleware
if (c.var.isRealtime === undefined) {
throw new Error(`Invariant(canAccelerateMiddleware): isRealtime middleware required`);
}
Expand Down
Loading