Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/moody-tips-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensindexer": minor
---

Introduces a worker that writes serialized representations of ENSIndexer Public Config and Indexing Status to ENSDb.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Introduces a worker that writes serialized representations of ENSIndexer Public Config and Indexing Status to ENSDb.
Introduces a worker that writes serialized representations of `ENSNodeMetadata` to ENSDb.

5 changes: 5 additions & 0 deletions .changeset/rich-buttons-shine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensnode-schema": minor
---

Includes schema for `ENSNodeMetadata`.
4 changes: 2 additions & 2 deletions apps/ensapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
"date-fns": "catalog:",
"drizzle-orm": "catalog:",
"hono": "catalog:",
"p-memoize": "^8.0.0",
"p-retry": "^7.1.0",
"p-memoize": "catalog:",
"p-retry": "catalog:",
"pg-connection-string": "catalog:",
"pino": "catalog:",
"ponder-enrich-gql-docs-middleware": "^0.1.3",
Expand Down
4 changes: 4 additions & 0 deletions apps/ensindexer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@
"@ensnode/ensnode-sdk": "workspace:*",
"@ensnode/ensrainbow-sdk": "workspace:*",
"@ensnode/ponder-metadata": "workspace:*",
"@ponder/client": "catalog:",
"caip": "catalog:",
"date-fns": "catalog:",
"deepmerge-ts": "^7.1.5",
"dns-packet": "^5.6.1",
"drizzle-orm": "catalog:",
"p-retry": "catalog:",
"pg-connection-string": "catalog:",
"hono": "catalog:",
"ponder": "catalog:",
Expand All @@ -43,6 +46,7 @@
"@ensnode/shared-configs": "workspace:*",
"@types/dns-packet": "^5.6.5",
"@types/node": "catalog:",
"@types/pg": "8.16.0",
"typescript": "catalog:",
"vitest": "catalog:"
}
Expand Down
134 changes: 134 additions & 0 deletions apps/ensindexer/ponder/src/ensdb-writer-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* This file manages syncing ENSNode metadata:
* - ENSIndexer Public Config
* - Indexing Status
* into the ENSDb.
Comment on lines +2 to +5
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* This file manages syncing ENSNode metadata:
* - ENSIndexer Public Config
* - Indexing Status
* into the ENSDb.
* This file manages upserting ENSNodeMetadata into ENSDb.

*/
import { secondsToMilliseconds } from "date-fns";
import pRetry from "p-retry";

import {
CrossChainIndexingStatusSnapshot,
type Duration,
ENSIndexerPublicConfig,
IndexingStatusResponseCodes,
OmnichainIndexingStatusIds,
validateENSIndexerPublicConfigCompatibility,
} from "@ensnode/ensnode-sdk";

Copy link
Member

Choose a reason for hiding this comment

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

Why does this file live under the /ponder directory?

import { EnsDbClient } from "@/lib/ensdb";
import { ensIndexerClient, waitForEnsIndexerToBecomeHealthy } from "@/lib/ensindexer";
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
import { ensIndexerClient, waitForEnsIndexerToBecomeHealthy } from "@/lib/ensindexer";
import { localEnsIndexerClient, waitForLocalEnsIndexerToBecomeHealthy } from "@/lib/ensindexer";

Do I understand this right?

Goal: It is confusing for someone reading the code here why ENSIndexer has its own client here. I appreciate how we're deep into this context now but goal is to optimize for someone reading this code 6 months from now.


const INDEXING_STATUS_RECORD_UPDATE_INTERVAL: Duration = 1;

/**
* ENSDb Writer Worker
*
* Runs the following tasks:
* 1) On application startup, attempt to upsert serialized representation of
* {@link ENSIndexerPublicConfig} into ENSDb.
* 2) On application startup, and then on recurring basis,
* following the {@link INDEXING_STATUS_RECORD_UPDATE_INTERVAL}, attempt to
* upsert serialized representation of {@link CrossChainIndexingStatusSnapshot}
* into ENSDb.
*/
async function ensDbWriterWorker() {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
async function ensDbWriterWorker() {
async function ensDbMetadataUpsertWorker() {

Is that fair? Seems much more accurate.

Assuming so, please rename all the related things / comments / files / etc.

console.log("ENSDb Writer Worker: waiting for ENSIndexer to become healthy.");

// 0. Wait for ENSIndexer to become healthy before running the worker's logic
Copy link
Member

Choose a reason for hiding this comment

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

Please take special note of this feedback.

When writing comments, you shouldn't essentially just write the same thing as the code is saying, but in English. This violates best practices.

Instead, your comments should explain: WHY? Why are we waiting for ENSIndexer to become healthy before running the worker's logic? Why is this valuable? What issue are we trying to avoid by doing this?

Please apply this idea across all your work forever in the future. Thanks.

await waitForEnsIndexerToBecomeHealthy;

console.log("ENSDb Writer Worker: ENSIndexer is healthy, starting tasks.");

// 1. Create ENSDb Client
const ensDbClient = new EnsDbClient();
Copy link
Member

Choose a reason for hiding this comment

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

Hmm. I think we need to give more of a detailed focus on the case of a completely fresh DATABASE_SCHEMA and how this is initialized by Ponder.

We have the existing code path:

  1. ENSIndexer fetches ENSRainbow config from ENSRainbow
  2. ENSIndexer can now build its own config
  3. ENSIndexer uses its own config to build a Ponder config
  4. Ponder uses the Ponder config to initialize DATABASE_SCHEMA.
  5. ENSIndexer now writes metadata into the given DATABASE_SCHEMA.

Right? Don't we need to wait until step 4 for the tables in DATABASE_SCHEMA to be initialized before we can perform step 5?

Assuming so, this waiting potentially needs to grow a lot if we add logic to wait up to an hour for step 1 above to be complete, waiting for ENSRainbow to become both healthy and ready.

And during all of this time, what is ENSApi supposed to do? From its perspective, it's been configured to read from DATABASE_SCHEMA but ENSIndexer might wait over an hour to create it.

One idea is that maybe we don't want to write at all to DATABASE_SCHEMA and instead we create our own database schema name that is always used by ENSNode instances writing to a particular Postgres database, kind of how Ponder sync works and how it can be shared across multiple Ponder instances at the same time. If we took this path we would need to update the data model so that multiple ENSNode instances could operate in parallel without impacting each others state. There's a meaningful amount of complexity here and we need to be careful not to mess it up. I don't have a strong opinion at the moment on the solution, above is just a quick brainstorm.

One thing that maybe could help here is to fix the situation where ENSIndexer can't initialize Ponder for more than an hour. Maybe we can make it so that ENSRainbow returns its config even before it is ready and as soon as it is healthy (which I imagine should be super fast?). If so then Ponder might be able to initialize the DATABASE_SCHEMA super fast, which would then allow us to start writing some symbolic value into the metadata tables that represents waiting for ENSRainbow to initialize which could then be read and understood by ENSApi during its own initialization phase. If we took this path then we need some strategy for what to do with Ponder's desire to just immediately start indexing as soon as we pass it its config. Is there a way to tell Ponder to initialize the database but don't start indexing yet?

Suggest to take a step back and carefully consider all cases and situations, then to document a plan and share it with the team for review 👍 Please feel welcome to challenge all the ideas I shared above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Short answer is that all we need to to is to wait for ENSIndexer /health endpoint to return 200 OK response.

Here are example logs from ponder application when database schema needs to be initialized:

12:54:08 PM INFO  database   Using database schema 'public'
12:54:09 PM INFO  database   Created tables [ensnode_metadata, reverse_name_records, node_resolver_relations, resolver_records, resolver_address_records, resolver_trecords, migrated_nodes, subregistries, registration_lifecycles, registrar_actions, _ensindexer_registrar_action_metadata, subgraph_domains, subgraph_accounts, subgraph_resolvers, subgraph_registrations, subgraph_wrapped_domains, subgraph_transfers, subgraph_new_owners, subgraph_new_resolvers, subgraph_new_ttls, subgraph_wrapped_transfers, subgraph_name_wrapped, subgraph_name_unwrapped, subgraph_fuses_set, subgraph_expiry_extended, subgraph_name_registered, subgraph_name_renewed, subgraph_name_transferred, subgraph_addr_changed, subgraph_multicoin_addr_changed, subgraph_name_changed, subgraph_abi_changed, subgraph_pubkey_changed, subgraph_text_changed, subgraph_contenthash_changed, subgraph_interface_changed, subgraph_authorisation_changed, subgraph_version_changed, name_sales, name_tokens]
12:54:09 PM INFO  server     Started listening on port 42069
12:54:09 PM INFO  server     Started returning 200 responses from /health endpoint

Please note how ponder first create tables, and only then starts returning 200 OK response from its /health endpoint.

Copy link
Contributor Author

@tk-o tk-o Dec 18, 2025

Choose a reason for hiding this comment

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

I like the idea for ENSApi to use specific DATABASE_SCHEMA. Suggest sticking to that. This way we can keep wrap the whole scope of ENSDb inside a single database schema — it's a very convenient assumption.

ENSApi has to wait for ENSDb to include EnsNodeMetadata. Otherwise, there's literally no use case ENSApi could serve. All ENSApi routes require indexing status information, which come from ENSDb.

ENSDb won't have the EnsNodeMetadata information until ENSIndexer stores it.

ENSIndexer won't store any EnsNodeMetadata information until ENSRainbow is healthy, and ENSIndexer is healthy.

I think we should create a "logical healthcheck" for ENSDb, such that clients connecting to ENSDb could know if the service is ready to be used. For example, we can consider this "logical healthcheck" to ensure that ENSDb includes EnsNodeMetadata for both keys: EnsNodeMetadataKeys.EnsIndexerPublicConfig, EnsNodeMetadataKeys.IndexingStatus.

Copy link
Member

Choose a reason for hiding this comment

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

@tk-o Overall agreed, but with a few distinctions:

  1. Suggest to separate the idea of "healthy" vs "ready".
  2. For an ENSDb client created with a given DATABASE_URL, DATABASE_SCHEMA, and expected database version value:
    1. It seems we might want to have some extremely simple operation (independent of DATABASE_SCHEMA where we just check if we can connect to the DATABASE_URL at all). Or maybe this should also check that DATABASE_SCHEMA exists too? Not sure right now. But anyway.. in my mind this is the definition of "healthy".
    2. We can have a different operation that requires that all 3 of the expected keys in the metadata table to be successfully read and deserialized AND for the key for the ENSDb version to match the expected version in the ENSDbClient. This operation could be defined as "ready".

What do you think?

These ideas might also influence how ENSApi implements both its /health and /ready APIs.


/**
* Handle ENSIndexerPublicConfig and ENSDb Version Records
*/
const handleEnsIndexerPublicConfigAndEnsDbVersionRecords = async () => {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const handleEnsIndexerPublicConfigAndEnsDbVersionRecords = async () => {
const initializeConfigMetadata = async () => {

Is that fair?

Copy link
Member

Choose a reason for hiding this comment

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

I note how this function doesn't initialize the metadata field for indexing status.

As a result, ENSDb clients have additional complexity in that they need to prepare for a situation where there may be a long duration of time where they can read 2 of the 3 metadata fields, but not the indexing status field.

What do you think about this? Appreciate your advice. Should we update this function so that it waits until all 3 metadata fields can be initialized at the same time, and then initializes them all together?

// Read stored config and in-memory config.
// Note: we wrap each operation in pRetry to ensure all of them can be
Copy link
Member

Choose a reason for hiding this comment

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

I'm not clear or confident on how this pRetry really works in reality.

What happens if this repeatedly fails? It just enters an infinite loop where it non-stop repeats these operations without any backoff and just slams them endlessly every nanosecond? And there would be no logging information at all about what's going on?

Appreciate if you can please ensure we always approach such operations in a very mature production-ready way.

To do this properly we need to optimize both for other devs reading this code as well as ENSNode operators with their own ENSNode instances working to debug some issue via logs.

// completed successfully.
const [storedConfig, inMemoryConfig] = await Promise.all([
pRetry(() => ensDbClient.getEnsIndexerPublicConfig()),
pRetry(() => ensIndexerClient.config()),
Copy link
Member

Choose a reason for hiding this comment

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

Is there a special reason why ENSIndexer is getting its own config through an API request to itself rather than just using the config object directly? If so, please document why we are taking this special approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, will document how the special reason is decoupling from dependencies that each of the fetched endpoint requires internally.

]);

// Validate in-memory config object compatibility with the stored one,
// if the stored one is available
if (storedConfig) {
try {
validateENSIndexerPublicConfigCompatibility(storedConfig, inMemoryConfig);
} catch (error) {
const errorMessage = `In-memory ENSIndexerPublicConfig object is not compatible with its counterpart stored in ENSDb.`;

// Throw the error to terminate the ENSIndexer process due to
// found config incompatibility
throw new Error(errorMessage, {
cause: error,
});
}
} else {
// Upsert ENSDb Version into ENSDb.
await ensDbClient.upsertEnsDbVersion(inMemoryConfig.versionInfo.ensDb);
Copy link
Member

Choose a reason for hiding this comment

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

I think we should change this operation into a pure insert, not an upsert, for safety. Right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we can leave it as is (upsert) and add the ENSDb version check as part of validateENSIndexerPublicConfigCompatibility function. This way, we'd get a guarantee that ENSDb version follows the ENSIndexerPublicConfig.versionInfo.ensDb value.

// Upsert ENSIndexerPublicConfig into ENSDb.
await ensDbClient.upsertEnsIndexerPublicConfig(inMemoryConfig);
Copy link
Member

Choose a reason for hiding this comment

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

Hmm... I think we have an issue here.

The logic flow here will only upsert the config if there's no stored config. But I don't believe that's right.

Whenever the config is confirmed as compatible, we should upsert it. We should note how there may be details in the config that are changing across time that are still compatible. We should always reflect the latest versions of these values in ENSDb via this upsert operation.

}
};

/**
* Handle Indexing Status Record Recursively
*/
const handleIndexingStatusRecordRecursively = async () => {
try {
// Read in-memory Indexing Status.
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't it be more accurate to say that we are fetching the indexing status locally through ENSIndexer's own API?

const inMemoryIndexingStatus = await ensIndexerClient.indexingStatus();

// Check if Indexing Status is available.
if (inMemoryIndexingStatus.responseCode !== IndexingStatusResponseCodes.Ok) {
throw new Error("Indexing Status must be available.");
Copy link
Member

Choose a reason for hiding this comment

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

We need to write better. This should explain why.

}

const { snapshot } = inMemoryIndexingStatus.realtimeProjection;
const { omnichainSnapshot } = snapshot;

// Check if Indexing Status is in expected status.
// The Omnichain Status must indicate that indexing has started already.
// Throw an error if Omnichain Status is "Unstarted".
if (omnichainSnapshot.omnichainStatus === OmnichainIndexingStatusIds.Unstarted) {
throw new Error("Omnichain Status must be different than 'Unstarted'.");
Copy link
Member

Choose a reason for hiding this comment

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

We need to explain why. Please see another comment where I gave a detailed suggestion for this.

}

// Upsert ENSIndexerPublicConfig into ENSDb.
await ensDbClient.upsertIndexingStatus(snapshot);
} catch (error) {
// Do nothing about this error, but having it logged.
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.error(`Could not upsert Indexing Status record due to: ${errorMessage}`);
} finally {
// Regardless of current iteration result,
// schedule the next callback to handle Indexing Status Record.
setTimeout(
handleIndexingStatusRecordRecursively,
secondsToMilliseconds(INDEXING_STATUS_RECORD_UPDATE_INTERVAL),
);
}
};

// 4. Handle ENSIndexer Public Config and ENSDb Version just once.
console.log("Task: store ENSIndexer Public Config and ENSDb Version in ENSDb.");
await handleEnsIndexerPublicConfigAndEnsDbVersionRecords();
console.log("ENSIndexer Public Config and ENSDb Version successfully stored in ENSDb.");

// 5. Handle Indexing Status on recurring basis.
console.log("Task: store Indexing Status in ENSDb.");
await handleIndexingStatusRecordRecursively();
console.log("Indexing Status successfully stored in ENSDb.");
}

// Run ENSDb Writer Worker in the background.
ensDbWriterWorker().catch((error) => {
console.error("ENSDb Writer Worker failed to perform its tasks", error);
process.exit(1);
});
25 changes: 25 additions & 0 deletions apps/ensindexer/src/lib/ensdb/drizzle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// This file was copied 1-to-1 from ENSApi.
Copy link
Collaborator

Choose a reason for hiding this comment

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

it doesn't seem trivial or possible to deduplicate this into ensnode-sdk, so let's wait until we have an ensnode-internal package or something to dry this up — can you add a TODO here indicating that? like

// TODO: deduplicate with apps/ensapi/src/lib/handlers/drizzle.ts when ensnode nodejs internal package is created

Copy link
Collaborator

Choose a reason for hiding this comment

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

likewise for logger.ts

Copy link
Collaborator

Choose a reason for hiding this comment

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

actually because ensindexer isn't using pino-formatted logs consistently, i think it might be best if we just use console.log in these cases. in which case i'd remove the debug logging in the client and let the consumer of the client handle all of the logging. in-client logging is only useful if we can indicate a log level to ignore it by default.

if you decide to keep pino in ensindexer, you'd want to add LogLevelEnvironment to the ENSIndexerEnvironment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I'm fine with using native logger from console object.

// TODO: deduplicate with apps/ensapi/src/lib/handlers/drizzle.ts when ensnode nodejs internal package is created

import { setDatabaseSchema } from "@ponder/client";
import { drizzle } from "drizzle-orm/node-postgres";

type Schema = { [name: string]: unknown };

/**
* Makes a Drizzle DB object.
*/
export const makeDrizzle = <SCHEMA extends Schema>({
schema,
databaseUrl,
databaseSchema,
}: {
schema: SCHEMA;
databaseUrl: string;
databaseSchema: string;
}) => {
// monkeypatch schema onto tables
setDatabaseSchema(schema, databaseSchema);

return drizzle(databaseUrl, { schema, casing: "snake_case" });
};
192 changes: 192 additions & 0 deletions apps/ensindexer/src/lib/ensdb/ensdb-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import config from "@/config";

import { eq } from "drizzle-orm/sql";

import * as schema from "@ensnode/ensnode-schema";
import {
type CrossChainIndexingStatusSnapshot,
deserializeCrossChainIndexingStatusSnapshot,
deserializeENSIndexerPublicConfig,
type ENSIndexerPublicConfig,
serializeCrossChainIndexingStatusSnapshotOmnichain,
serializeENSIndexerPublicConfig,
} from "@ensnode/ensnode-sdk";

import { makeDrizzle } from "./drizzle";
import {
EnsNodeMetadataKeys,
type SerializedEnsNodeMetadata,
type SerializedEnsNodeMetadataEnsDbVersion,
type SerializedEnsNodeMetadataEnsIndexerPublicConfig,
type SerializedEnsNodeMetadataIndexingStatus,
} from "./ensnode-metadata";

/**
* ENSDb Client Query
*
Includes methods for reading from ENSDb.
*/
export interface EnsDbClientQuery {
/**
* Get ENSDb Version
*
* @returns the existing record, or `undefined`.
* @throws if not exactly one record was found.
*/
getEnsDbVersion(): Promise<string | undefined>;

/**
* Get ENSIndexer Public Config
*
* @returns the existing record, or `undefined`.
* @throws if not exactly one record was found.
*/
getEnsIndexerPublicConfig(): Promise<ENSIndexerPublicConfig | undefined>;

/**
* Get Indexing Status
*
* @returns the existing record, or `undefined`.
* @throws if not exactly one record was found.
*/
getIndexingStatus(): Promise<CrossChainIndexingStatusSnapshot | undefined>;
}

/**
* ENSDb Client Mutation
*
* Includes methods for writing into ENSDb.
*/
export interface EnsDbClientMutation {
/**
* Upsert ENSDb Version
*
* @throws when upsert operation failed.
*/
upsertEnsDbVersion(ensDbVersion: string): Promise<void>;

/**
* Upsert ENSIndexer Public Config
*
* @throws when upsert operation failed.
*/
upsertEnsIndexerPublicConfig(ensIndexerPublicConfig: ENSIndexerPublicConfig): Promise<void>;

/**
* Upsert Indexing Status
*
* @throws when upsert operation failed.
*/
upsertIndexingStatus(indexingStatus: CrossChainIndexingStatusSnapshot): Promise<void>;
}

/**
* ENSDb Client
*/
export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation {
#db = makeDrizzle({
databaseSchema: config.databaseSchemaName,
databaseUrl: config.databaseUrl,
schema,
});

async getEnsDbVersion(): Promise<string | undefined> {
const record = await this.getEnsNodeMetadata<SerializedEnsNodeMetadataEnsDbVersion>({
key: EnsNodeMetadataKeys.EnsDbVersion,
});

return record;
}

async getEnsIndexerPublicConfig(): Promise<ENSIndexerPublicConfig | undefined> {
const record = await this.getEnsNodeMetadata<SerializedEnsNodeMetadataEnsIndexerPublicConfig>({
key: EnsNodeMetadataKeys.EnsIndexerPublicConfig,
});

if (!record) {
return undefined;
}

return deserializeENSIndexerPublicConfig(record);
}

async getIndexingStatus(): Promise<CrossChainIndexingStatusSnapshot | undefined> {
const record = await this.getEnsNodeMetadata<SerializedEnsNodeMetadataIndexingStatus>({
key: EnsNodeMetadataKeys.IndexingStatus,
});

if (!record) {
return undefined;
}

return deserializeCrossChainIndexingStatusSnapshot(record);
}

async upsertEnsDbVersion(ensDbVersion: string): Promise<void> {
await this.upsertEnsNodeMetadata({
key: EnsNodeMetadataKeys.EnsDbVersion,
value: ensDbVersion,
});
}

async upsertEnsIndexerPublicConfig(
ensIndexerPublicConfig: ENSIndexerPublicConfig,
): Promise<void> {
await this.upsertEnsNodeMetadata({
key: EnsNodeMetadataKeys.EnsIndexerPublicConfig,
value: serializeENSIndexerPublicConfig(ensIndexerPublicConfig),
});
}

async upsertIndexingStatus(indexingStatus: CrossChainIndexingStatusSnapshot): Promise<void> {
await this.upsertEnsNodeMetadata({
key: EnsNodeMetadataKeys.IndexingStatus,
value: serializeCrossChainIndexingStatusSnapshotOmnichain(indexingStatus),
});
}

/**
* Get ENSNode metadata record
*
* @returns selected record in ENSDb.
* @throws when exactly one matching metadata record was not found
*/
private async getEnsNodeMetadata<
EnsNodeMetadataType extends SerializedEnsNodeMetadata = SerializedEnsNodeMetadata,
>(metadata: Pick<EnsNodeMetadataType, "key">): Promise<EnsNodeMetadataType["value"] | undefined> {
const result = await this.#db
.select()
.from(schema.ensNodeMetadata)
.where(eq(schema.ensNodeMetadata.key, metadata.key));

if (result.length === 0) {
return undefined;
}

if (result.length === 1 && result[0]) {
return result[0].value as EnsNodeMetadataType["value"];
}

throw new Error(`There must be exactly one ENSNodeMetadata record for '${metadata.key}' key`);
}

/**
* Upsert ENSNode metadata
*
* @throws when upsert operation failed.
*/
private async upsertEnsNodeMetadata<
EnsNodeMetadataType extends SerializedEnsNodeMetadata = SerializedEnsNodeMetadata,
>(metadata: EnsNodeMetadataType): Promise<void> {
await this.#db
.insert(schema.ensNodeMetadata)
.values({
key: metadata.key,
value: metadata.value,
})
.onConflictDoUpdate({
target: schema.ensNodeMetadata.key,
set: { value: metadata.value },
});
}
}
Loading