Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4b125d0
Introduce `IndexingMetadataContext` data model to ENSNode SDK
tk-o Apr 24, 2026
dcad533
Create `init*` functions for running event handlers preconditions
tk-o Apr 24, 2026
a5a8bd6
Execute `migrateEnsNodeSchema` from inside of `initIndexingSetup` fun…
tk-o Apr 24, 2026
c57c2ac
Use `EnsIndexerStackInfo` for `stackInfo` field in `IndexingMetadataC…
tk-o Apr 24, 2026
a28b0de
Add `getIndexingMetadataContext` method to `EnsDbReader` class
tk-o Apr 24, 2026
e91bd13
Add `upsertIndexingMetadataContext` method to `EnsDbWriter` class
tk-o Apr 24, 2026
8892dd5
Update `EnsDbWriterWorker` class to serve a new limited role
tk-o Apr 24, 2026
ee1190e
Fix ponder build issue
tk-o Apr 24, 2026
4954e21
Fix typos
tk-o Apr 24, 2026
2f8532e
Implement full tasks sequence for `initIndexingOnchainEvents`
tk-o Apr 25, 2026
361e99d
Merge `initIndexingSetup` function into `initIndexingOnchainEvents`
tk-o Apr 25, 2026
7595a41
Update unit tests
tk-o Apr 25, 2026
1902bfb
Use `getIndexingMetadataContext` for all ENSNode Metadata reads from …
tk-o Apr 25, 2026
6bda3d6
Merge remote-tracking branch 'origin/main' into feat/indexing-metadat…
tk-o Apr 25, 2026
e3ddda0
Simplify `EnsDbReader` class and `EnsDbWriter` class
tk-o Apr 25, 2026
4684fb4
Fix tests
tk-o Apr 25, 2026
1ff960e
Apply AI PR feedback
tk-o Apr 26, 2026
41060d0
Simplify `initIndexingOnchainEvents` logic
tk-o Apr 26, 2026
e1d6d04
Update unit tests
tk-o Apr 26, 2026
98e6c45
Simplify `initIndexingOnchainEvents` function
tk-o Apr 26, 2026
eebe386
Simplify logic in ENSIndexer HTTP endpoints
tk-o Apr 26, 2026
11711ca
Improve naming
tk-o Apr 26, 2026
89c974a
Make indexing status cache and stack info cache for ENSApi to use the…
tk-o Apr 27, 2026
292ed35
Merge remote-tracking branch 'origin/main' into feat/indexing-metadat…
tk-o Apr 27, 2026
159c4ff
Improve code docs
tk-o Apr 27, 2026
b36418b
Create a mock file for config.schema.ts
tk-o Apr 27, 2026
f3355ef
Integrate ENSDb health check and readiness check into `initIndexingOn…
tk-o Apr 27, 2026
40961af
Update code docs
tk-o Apr 27, 2026
6279dea
Apply AI PR feedback
tk-o Apr 27, 2026
43ef45b
Merge remote-tracking branch 'origin/main' into feat/indexing-metadat…
tk-o Apr 27, 2026
ad1564f
Apply AI PR feedback
tk-o Apr 28, 2026
110927d
Add changeset
tk-o Apr 28, 2026
bcd81fe
Merge remote-tracking branch 'origin/main' into feat/indexing-metadat…
tk-o Apr 28, 2026
698191f
Apply AI PR feedback
tk-o Apr 28, 2026
725901f
Fix re-exports
tk-o Apr 29, 2026
55ae265
Merge remote-tracking branch 'origin/main' into feat/indexing-metadat…
tk-o Apr 29, 2026
84b52e1
Create ENSRainbow readiness logic inline in ENSIndexer
tk-o Apr 29, 2026
5cc44ea
Fix typo
tk-o Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions apps/ensindexer/ponder/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,11 @@ import { cors } from "hono/cors";

import type { ErrorResponse } from "@ensnode/ensnode-sdk";

import { migrateEnsNodeSchema } from "@/lib/ensdb/migrate-ensnode-schema";
import { startEnsDbWriterWorker } from "@/lib/ensdb-writer-worker/singleton";
import { logger } from "@/lib/logger";

import ensNodeApi from "./handlers/ensnode-api";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Should we rename this? It seems strange to call it an "ENSNode API".


Comment thread
tk-o marked this conversation as resolved.
Outdated
// Before starting the ENSDb Writer Worker, we need to ensure that
// the ENSNode Schema in ENSDb is up to date by running any pending migrations.
await migrateEnsNodeSchema().catch((error) => {
logger.error({
msg: "Failed to initialize ENSNode metadata",
error,
module: "ponder-api",
});
process.exitCode = 1;
throw error;
});

// The entry point for the ENSDb Writer Worker.
startEnsDbWriterWorker();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { waitForEnsRainbowToBeReady } from "@/lib/ensrainbow/singleton";

/**
* Prepare for executing the "onchain" event handlers.
*
* During Ponder startup, the "onchain" event handlers are executed
* after all "setup" event handlers have completed.
*
* This function is useful to make sure any long-running preconditions for
* onchain event handlers are met, for example, waiting for
* the ENSRainbow instance to be ready before processing any onchain events
* that require data from ENSRainbow.
*
* @example A single blocking precondition
* ```ts
* await waitForEnsRainbowToBeReady();
* ```
*
* @example Multiple blocking preconditions
* ```ts
* await Promise.all([
* waitForEnsRainbowToBeReady(),
* waitForAnotherPrecondition(),
* ]);
* ```
*/
export async function initIndexingOnchainEvents(): Promise<void> {
await waitForEnsRainbowToBeReady();
}
53 changes: 53 additions & 0 deletions apps/ensindexer/src/lib/indexing-engines/init-indexing-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* This module defines the initialization logic for the setup handlers of
* the Ponder indexing engine executed in an ENSIndexer instance.
*
* Setup handlers are executed by Ponder once per ENSIndexer instance lifetime,
* at the start of the omnichain indexing process.
*
* ENSIndexer startup sequence executed by Ponder:
* 1. Connect to the database and initialize required database objects.
* 2. Start the omnichain indexing process.
* 3. Check whether Ponder Checkpoints are already initialized.
* 4. If not:
* a) Execute setup handlers.
* b) Initialize Ponder Checkpoints.
* 5. a) Make Ponder HTTP API usable.
* 5. b) Start executing "onchain" event handlers.
*
* Step 4 is skipped on ENSIndexer instance restart if Ponder Checkpoints were
* already initialized in a previous run.
*/

import { logger } from "@/lib/logger";

/**
* Initialize indexing setup
*
* Runs once per ENSIndexer instance lifetime to initialize indexing setup.
*
* Since multiple ENSIndexer instances may run concurrently against the same
* ENSDb instance, this function MUST BE idempotent and race-condition-safe.
*
* Completion of this function unblocks the following sequence of events
* during ENSIndexer startup:
* 1. "setup" event handlers execute
* 2. Ponder Checkpoints initialize
* 3. IndexingStatusBuilder can build OmnichainIndexingStatusSnapshot
* via LocalPonderClient (which queries the Ponder HTTP API)
*
* @throws Error if any precondition is not satisfied.
*/
export async function initIndexingSetup(): Promise<void> {
const { migrateEnsNodeSchema } = await import("@/lib/ensdb/migrate-ensnode-schema");
Comment thread
tk-o marked this conversation as resolved.
Outdated
// Ensure the ENSNode Schema in ENSDb is up to date by running any pending migrations.
await migrateEnsNodeSchema().catch((error) => {
logger.error({
msg: "Failed to initialize ENSNode metadata",
error,
module: "ponder-api",
Comment thread
tk-o marked this conversation as resolved.
Outdated
Comment thread
vercel[bot] marked this conversation as resolved.
Outdated
Comment thread
tk-o marked this conversation as resolved.
Outdated
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
process.exitCode = 1;
throw error;
});
}
Comment thread
tk-o marked this conversation as resolved.
Outdated
24 changes: 24 additions & 0 deletions apps/ensindexer/src/lib/indexing-engines/ponder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ const { mockPonderOn } = vi.hoisted(() => ({ mockPonderOn: vi.fn() }));

const mockWaitForEnsRainbow = vi.hoisted(() => vi.fn());

const mockMigrateEnsNodeSchema = vi.hoisted(() => vi.fn());

// Set up PONDER_COMMON global before any imports that depend on it
vi.hoisted(() => {
(globalThis as any).PONDER_COMMON = {
options: {
command: "start",
port: 42069,
},
logger: {
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
};
});
Comment thread
tk-o marked this conversation as resolved.

vi.mock("ponder:registry", () => ({
ponder: {
on: (...args: unknown[]) => mockPonderOn(...args),
Expand All @@ -21,10 +40,15 @@ vi.mock("@/lib/ensrainbow/singleton", () => ({
waitForEnsRainbowToBeReady: mockWaitForEnsRainbow,
}));

vi.mock("@/lib/ensdb/migrate-ensnode-schema", () => ({
migrateEnsNodeSchema: mockMigrateEnsNodeSchema,
}));

Comment thread
tk-o marked this conversation as resolved.
Outdated
describe("addOnchainEventListener", () => {
beforeEach(async () => {
vi.clearAllMocks();
mockWaitForEnsRainbow.mockResolvedValue(undefined);
mockMigrateEnsNodeSchema.mockResolvedValue(undefined);
// Reset module state to test idempotent behavior correctly
vi.resetModules();
});
Expand Down
92 changes: 32 additions & 60 deletions apps/ensindexer/src/lib/indexing-engines/ponder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
ponder,
} from "ponder:registry";

import { waitForEnsRainbowToBeReady } from "@/lib/ensrainbow/singleton";
import { initIndexingOnchainEvents } from "./init-indexing-onchain-events";
import { initIndexingSetup } from "./init-indexing-setup";

/**
* Context passed to event handlers registered with
Expand Down Expand Up @@ -113,7 +114,7 @@ const EventTypeIds = {
*
* Driven by an onchain event emitted by an indexed contract.
*/
Onchain: "Onchain",
OnchainEvent: "OnchainEvent",
} as const;

/**
Expand All @@ -125,59 +126,13 @@ function buildEventTypeId(eventName: EventNames): EventTypeId {
if (eventName.endsWith(":setup")) {
return EventTypeIds.Setup;
} else {
return EventTypeIds.Onchain;
return EventTypeIds.OnchainEvent;
}
}

/**
* Prepare for executing the "setup" event handlers.
*
* During Ponder startup, the "setup" event handlers are executed:
* - After Ponder completed database migrations for ENSIndexer Schema in ENSDb.
* - Before Ponder starts processing any onchain events for indexed chains.
*
* This function is useful to make sure ENSDb is ready for writes, for example,
* by ensuring all required Postgres extensions are installed, etc.
*/
async function initializeIndexingSetup(): Promise<void> {
/**
* Setup event handlers should not have any *long-running* preconditions. This is because
* Ponder populates the indexing metrics for all indexed chains only after all setup handlers have run.
* ENSIndexer relies on these indexing metrics being immediately available on startup to build and
* store the current Indexing Status in ENSDb.
*/
}

/**
* Prepare for executing the "onchain" event handlers.
*
* During Ponder startup, the "onchain" event handlers are executed
* after all "setup" event handlers have completed.
*
* This function is useful to make sure any long-running preconditions for
* onchain event handlers are met, for example, waiting for
* the ENSRainbow instance to be ready before processing any onchain events
* that require data from ENSRainbow.
*
* @example A single blocking precondition
* ```ts
* await waitForEnsRainbowToBeReady();
* ```
*
* @example Multiple blocking preconditions
* ```ts
* await Promise.all([
* waitForEnsRainbowToBeReady(),
* waitForAnotherPrecondition(),
* ]);
* ```
*/
async function initializeIndexingActivation(): Promise<void> {
await waitForEnsRainbowToBeReady();
}

let eventHandlerPreconditionsFullyExecuted = false;
let indexingSetupPromise: Promise<void> | null = null;
let indexingActivationPromise: Promise<void> | null = null;
let indexingOnchainEventsPromise: Promise<void> | null = null;

/**
* Execute any necessary preconditions before running an event handler
Expand All @@ -192,25 +147,42 @@ let indexingActivationPromise: Promise<void> | null = null;
* "onchain" event.
*/
async function eventHandlerPreconditions(eventType: EventTypeId): Promise<void> {
if (eventHandlerPreconditionsFullyExecuted) {
// Preconditions have already been fully executed, so we can skip executing them again.
// We can also reset the promises for indexing setup and onchain events to free up memory,
// since they will never be used again after the preconditions have been fully executed.
Comment thread
tk-o marked this conversation as resolved.
Outdated
indexingSetupPromise = null;
indexingOnchainEventsPromise = null;
return;
}

switch (eventType) {
case EventTypeIds.Setup: {
if (indexingSetupPromise === null) {
// Initialize the indexing setup just once.
indexingSetupPromise = initializeIndexingSetup();
// Init the indexing setup just once. There will be multiple
// setup events executed during Ponder startup, but they will
// run sequentially, so we can just check if we have already
// initialized the indexing setup or not.
indexingSetupPromise = initIndexingSetup();
Comment thread
tk-o marked this conversation as resolved.
Outdated
}

return await indexingSetupPromise;
}

case EventTypeIds.Onchain: {
if (indexingActivationPromise === null) {
// Initialize the indexing activation just once in order to
// optimize the "hot path" of indexing onchain events, since these are
// much more frequent than setup events.
indexingActivationPromise = initializeIndexingActivation();
case EventTypeIds.OnchainEvent: {
if (indexingOnchainEventsPromise === null) {
// Init the indexing of "onchain" events just once in order to
// optimize the indexing "hot path", since these events are much
// more frequent than setup events.
indexingOnchainEventsPromise = initIndexingOnchainEvents().then(() => {
// Mark the preconditions as fully executed after the first time we execute
// the preconditions for onchain events, since that's the "hot path" and we want to
// minimize the overhead of this function in the long run.
eventHandlerPreconditionsFullyExecuted = true;
});
}

return await indexingActivationPromise;
return await indexingOnchainEventsPromise;
}
Comment thread
tk-o marked this conversation as resolved.
}
}
Comment thread
tk-o marked this conversation as resolved.
Expand Down
1 change: 1 addition & 0 deletions packages/ensnode-sdk/src/ensnode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export {
} from "./client";
export * from "./client-error";
export * from "./deployments";
export * from "./metadata";
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { prettifyError } from "zod/v4";

import { buildUnvalidatedCrossChainIndexingStatusSnapshot } from "../../../indexing-status";
import type { Unvalidated } from "../../../shared/types";
import { buildUnvalidatedEnsNodeStackInfo } from "../../../stack-info";
import {
type IndexingMetadataContext,
type IndexingMetadataContextInitialized,
IndexingMetadataContextStatusCodes,
} from "../indexing-metadata-context";
import type {
SerializedIndexingMetadataContext,
SerializedIndexingMetadataContextInitialized,
} from "../serialize/indexing-metadata-context";
import {
makeIndexingMetadataContextSchema,
makeSerializedIndexingMetadataContextSchema,
} from "../zod-schemas/indexing-metadata-context";

/**
* Builds an unvalidated {@link IndexingMetadataContextInitialized} object.
*/
function buildUnvalidatedIndexingMetadataContextInitializedSchema(
serializedIndexingMetadataContext: SerializedIndexingMetadataContextInitialized,
): Unvalidated<IndexingMetadataContextInitialized> {
return {
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
statusCode: serializedIndexingMetadataContext.statusCode,
indexingStatus: buildUnvalidatedCrossChainIndexingStatusSnapshot(
serializedIndexingMetadataContext.indexingStatus,
),
stackInfo: buildUnvalidatedEnsNodeStackInfo(serializedIndexingMetadataContext.stackInfo),
};
}

/**
* Builds an unvalidated {@link IndexingMetadataContext} object to be
* validated with {@link makeIndexingMetadataContextSchema}.
*
* @param serializedIndexingMetadataContext - The serialized indexing metadata context to build from.
* @return An unvalidated {@link IndexingMetadataContextInitialized} object.
Comment thread
vercel[bot] marked this conversation as resolved.
Outdated
*/
function buildUnvalidatedIndexingMetadataContextSchema(
serializedIndexingMetadataContext: SerializedIndexingMetadataContext,
): Unvalidated<IndexingMetadataContext> {
Comment thread
tk-o marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
switch (serializedIndexingMetadataContext.statusCode) {
case IndexingMetadataContextStatusCodes.Uninitialized:
return serializedIndexingMetadataContext;

case IndexingMetadataContextStatusCodes.Initialized:
return buildUnvalidatedIndexingMetadataContextInitializedSchema(
serializedIndexingMetadataContext,
);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
tk-o marked this conversation as resolved.

/**
* Deserialize a serialized {@link IndexingMetadataContext} object.
*/
export function deserializeIndexingMetadataContext(
serializedIndexingMetadataContext: Unvalidated<SerializedIndexingMetadataContext>,
valueLabel?: string,
): IndexingMetadataContext {
const label = valueLabel ?? "IndexingMetadataContext";

const parsed = makeSerializedIndexingMetadataContextSchema(label)
.transform(buildUnvalidatedIndexingMetadataContextSchema)
.pipe(makeIndexingMetadataContextSchema(label))
.safeParse(serializedIndexingMetadataContext);

if (parsed.error) {
throw new Error(`Cannot validate IndexingMetadataContext:\n${prettifyError(parsed.error)}\n`);
Comment thread
tk-o marked this conversation as resolved.
Outdated
Comment thread
vercel[bot] marked this conversation as resolved.
Outdated
}
return parsed.data;
}
4 changes: 4 additions & 0 deletions packages/ensnode-sdk/src/ensnode/metadata/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./deserialize/indexing-metadata-context";
export * from "./indexing-metadata-context";
export * from "./serialize/indexing-metadata-context";
export * from "./validate/indexing-metadata-context";
Loading
Loading