Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 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
10 changes: 10 additions & 0 deletions apps/ensapi/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ ENSINDEXER_URL=http://localhost:42069
# NOTE that ENSApi does NOT need to define DATABASE_SCHEMA, as it is inferred from the connected ENSIndexer's Config.
Comment thread
tk-o marked this conversation as resolved.
Outdated
DATABASE_URL=postgresql://dbuser:abcd1234@localhost:5432/my_database

# ENSDb: schema name
Comment thread
tk-o marked this conversation as resolved.
# Required. This is a namespace for the tables that the ENSDbClient for ENSApi will apply to read ENSDb data.

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.

Suggest avoiding the use of the word "namespace" here -- it's overloading terminology too much with ENS namespace.

# Must be set to an existing namespace in the connected ENSDb at DATABASE_URL.
DATABASE_SCHEMA=public
Comment thread
tk-o marked this conversation as resolved.
Outdated
Comment thread
tk-o marked this conversation as resolved.
Outdated

# ENS Namespace Configuration
# Required. Must be an ENS namespace's Identifier such as mainnet, sepolia, or ens-test-env.
# (see `@ensnode/datasources` for available options).
NAMESPACE=mainnet

# ENSApi: RPC Configuration
# Required. ENSApi requires an HTTP RPC to the connected ENSIndexer's ENS Root Chain, which depends

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.

Please invest time in carefully crafting a refinement to how this idea is documented. We need our language here to be precisely aligned with the new architecture refinements.

# on ENSIndexer's NAMESPACE (ex: mainnet, sepolia, ens-test-env). This ENS Root Chain RPC
Expand Down
29 changes: 12 additions & 17 deletions apps/ensapi/src/cache/indexing-status.cache.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import config from "@/config";

import {
type CrossChainIndexingStatusSnapshot,
ENSNodeClient,
IndexingStatusResponseCodes,
SWRCache,
} from "@ensnode/ensnode-sdk";
import { type CrossChainIndexingStatusSnapshot, SWRCache } from "@ensnode/ensnode-sdk";

import { ensDbClient } from "@/lib/ensdb-client/singleton";
import { makeLogger } from "@/lib/logger";

const logger = makeLogger("indexing-status.cache");
const client = new ENSNodeClient({ url: config.ensIndexerUrl });

export const indexingStatusCache = new SWRCache<CrossChainIndexingStatusSnapshot>({
fn: async (_cachedResult) =>
client
.indexingStatus() // fetch a new indexing status snapshot
.then((response) => {
if (response.responseCode !== IndexingStatusResponseCodes.Ok) {
// An indexing status response was successfully fetched, but the response code contained within the response was not 'ok'.
ensDbClient
.getIndexingStatusSnapshot() // get the latest indexing status snapshot
.then((snapshot) => {
if (snapshot === undefined) {
// An indexing status snapshot has not been found in ENSDb yet.
// This might happen during application startup, i.e. when ENSDb
// has not yet been populated with the first snapshot.
// Therefore, throw an error to trigger the subsequent `.catch` handler.
throw new Error("Received Indexing Status response with responseCode other than 'ok'.");
throw new Error("Indexing Status snapshot not found in ENSDb yet.");
}

logger.info("Fetched Indexing Status to be cached");
Expand All @@ -29,10 +24,10 @@ export const indexingStatusCache = new SWRCache<CrossChainIndexingStatusSnapshot
// Therefore, return it so that this current invocation of `readCache` will:
// - Replace the currently cached value (if any) with this new value.
// - Return this non-null value.
return response.realtimeProjection.snapshot;
return snapshot;
})
.catch((error) => {
// Either the indexing status snapshot fetch failed, or the indexing status response was not 'ok'.
// Either the indexing status snapshot fetch failed, or the indexing status snapshot was not found in ENSDb yet.
// Therefore, throw an error so that this current invocation of `readCache` will:
// - Reject the newly fetched response (if any) such that it won't be cached.
// - Return the most recently cached value from prior invocations, or `null` if no prior invocation successfully cached a value.
Expand Down
93 changes: 1 addition & 92 deletions apps/ensapi/src/config/config.schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
} from "@ensnode/ensnode-sdk";
import type { RpcConfig } from "@ensnode/ensnode-sdk/internal";

import { buildConfigFromEnvironment, buildEnsApiPublicConfig } from "@/config/config.schema";
import { buildConfigFromEnvironment } from "@/config/config.schema";
import { ENSApi_DEFAULT_PORT } from "@/config/defaults";
import type { EnsApiEnvironment } from "@/config/environment";
import logger from "@/lib/logger";
Expand Down Expand Up @@ -64,7 +64,7 @@
json: () => Promise.resolve(serializeENSIndexerPublicConfig(ENSINDEXER_PUBLIC_CONFIG)),
});

await expect(buildConfigFromEnvironment(BASE_ENV)).resolves.toStrictEqual({

Check failure on line 67 in apps/ensapi/src/config/config.schema.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests

src/config/config.schema.test.ts > buildConfigFromEnvironment > returns a valid config object using environment variables

TypeError: You must provide a Promise to expect() when using .resolves, not 'undefined'. ❯ src/config/config.schema.test.ts:67:54
port: ENSApi_DEFAULT_PORT,
databaseUrl: BASE_ENV.DATABASE_URL,
ensIndexerUrl: new URL(BASE_ENV.ENSINDEXER_URL),
Expand Down Expand Up @@ -99,7 +99,7 @@
CUSTOM_REFERRAL_PROGRAM_EDITIONS: customUrl,
});

expect(config.customReferralProgramEditionConfigSetUrl).toEqual(new URL(customUrl));

Check failure on line 102 in apps/ensapi/src/config/config.schema.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests

src/config/config.schema.test.ts > buildConfigFromEnvironment > parses CUSTOM_REFERRAL_PROGRAM_EDITIONS as a URL object

TypeError: Cannot read properties of undefined (reading 'customReferralProgramEditionConfigSetUrl') ❯ src/config/config.schema.test.ts:102:19
});

describe("Useful error messages", () => {
Expand Down Expand Up @@ -130,7 +130,7 @@
CUSTOM_REFERRAL_PROGRAM_EDITIONS: "not-a-url",
});

expect(logger.error).toHaveBeenCalledWith(

Check failure on line 133 in apps/ensapi/src/config/config.schema.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests

src/config/config.schema.test.ts > buildConfigFromEnvironment > Useful error messages > logs error and exits when CUSTOM_REFERRAL_PROGRAM_EDITIONS is not a valid URL

AssertionError: expected "vi.fn()" to be called with arguments: [ StringContaining{…} ] Received: 1st vi.fn() call: [ - StringContaining "CUSTOM_REFERRAL_PROGRAM_EDITIONS is not a valid URL: not-a-url", + "Failed to parse environment configuration: + ✖ Invalid NAMESPACE. Got 'undefined', but supported ENS namespaces are: Mainnet, Sepolia, SepoliaV2, EnsTestEnv + ", ] Number of calls: 1 ❯ src/config/config.schema.test.ts:133:28
expect.stringContaining("CUSTOM_REFERRAL_PROGRAM_EDITIONS is not a valid URL: not-a-url"),
);
expect(process.exit).toHaveBeenCalledWith(1);
Expand All @@ -147,7 +147,7 @@
QUICKNODE_API_KEY: "my-api-key",
});

expect(logger.error).toHaveBeenCalledWith(

Check failure on line 150 in apps/ensapi/src/config/config.schema.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests

src/config/config.schema.test.ts > buildConfigFromEnvironment > Useful error messages > logs error message when QuickNode RPC config was partially configured (missing endpoint name)

AssertionError: expected "vi.fn()" to be called with arguments: [ …(2) ] Received: 1st vi.fn() call: [ - Error { - "message": "Use of the QUICKNODE_API_KEY environment variable requires use of the QUICKNODE_ENDPOINT_NAME environment variable as well.", - }, - "Failed to build EnsApiConfig", + "Failed to parse environment configuration: + ✖ Invalid NAMESPACE. Got 'undefined', but supported ENS namespaces are: Mainnet, Sepolia, SepoliaV2, EnsTestEnv + ", ] Number of calls: 1 ❯ src/config/config.schema.test.ts:150:28
new Error(
"Use of the QUICKNODE_API_KEY environment variable requires use of the QUICKNODE_ENDPOINT_NAME environment variable as well.",
),
Expand All @@ -167,7 +167,7 @@
QUICKNODE_ENDPOINT_NAME: "my-endpoint-name",
});

expect(logger.error).toHaveBeenCalledWith(

Check failure on line 170 in apps/ensapi/src/config/config.schema.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests

src/config/config.schema.test.ts > buildConfigFromEnvironment > Useful error messages > logs error message when QuickNode RPC config was partially configured (missing API key)

AssertionError: expected "vi.fn()" to be called with arguments: [ …(2) ] Received: 1st vi.fn() call: [ - Error { - "message": "Use of the QUICKNODE_ENDPOINT_NAME environment variable requires use of the QUICKNODE_API_KEY environment variable as well.", - }, - "Failed to build EnsApiConfig", + "Failed to parse environment configuration: + ✖ Invalid NAMESPACE. Got 'undefined', but supported ENS namespaces are: Mainnet, Sepolia, SepoliaV2, EnsTestEnv + ", ] Number of calls: 1 ❯ src/config/config.schema.test.ts:170:28
new Error(
"Use of the QUICKNODE_ENDPOINT_NAME environment variable requires use of the QUICKNODE_API_KEY environment variable as well.",
),
Expand All @@ -177,94 +177,3 @@
});
});
});

describe("buildEnsApiPublicConfig", () => {
it("returns a valid ENSApi public config with correct structure", () => {
const mockConfig = {
port: ENSApi_DEFAULT_PORT,
databaseUrl: BASE_ENV.DATABASE_URL,
ensIndexerUrl: new URL(BASE_ENV.ENSINDEXER_URL),
ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG,
namespace: ENSINDEXER_PUBLIC_CONFIG.namespace,
databaseSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName,
rpcConfigs: new Map([
[
1,
{
httpRPCs: [new URL(VALID_RPC_URL)],
websocketRPC: undefined,
} satisfies RpcConfig,
],
]),
customReferralProgramEditionConfigSetUrl: undefined,
};

const result = buildEnsApiPublicConfig(mockConfig);

expect(result).toStrictEqual({
version: packageJson.version,
theGraphFallback: {
canFallback: false,
reason: "not-subgraph-compatible",
},
ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG,
});
});

it("preserves the complete ENSIndexer public config structure", () => {
const mockConfig = {
port: ENSApi_DEFAULT_PORT,
databaseUrl: BASE_ENV.DATABASE_URL,
ensIndexerUrl: new URL(BASE_ENV.ENSINDEXER_URL),
ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG,
namespace: ENSINDEXER_PUBLIC_CONFIG.namespace,
databaseSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName,
rpcConfigs: new Map(),
customReferralProgramEditionConfigSetUrl: undefined,
};

const result = buildEnsApiPublicConfig(mockConfig);

// Verify that all ENSIndexer public config fields are preserved
expect(result.ensIndexerPublicConfig.namespace).toBe(ENSINDEXER_PUBLIC_CONFIG.namespace);
expect(result.ensIndexerPublicConfig.plugins).toEqual(ENSINDEXER_PUBLIC_CONFIG.plugins);
expect(result.ensIndexerPublicConfig.versionInfo).toEqual(ENSINDEXER_PUBLIC_CONFIG.versionInfo);
expect(result.ensIndexerPublicConfig.indexedChainIds).toEqual(
ENSINDEXER_PUBLIC_CONFIG.indexedChainIds,
);
expect(result.ensIndexerPublicConfig.isSubgraphCompatible).toBe(
ENSINDEXER_PUBLIC_CONFIG.isSubgraphCompatible,
);
expect(result.ensIndexerPublicConfig.labelSet).toEqual(ENSINDEXER_PUBLIC_CONFIG.labelSet);
expect(result.ensIndexerPublicConfig.databaseSchemaName).toBe(
ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName,
);
});

it("includes the theGraphFallback and redacts api key", () => {
const mockConfig = {
port: ENSApi_DEFAULT_PORT,
databaseUrl: BASE_ENV.DATABASE_URL,
ensIndexerUrl: new URL(BASE_ENV.ENSINDEXER_URL),
ensIndexerPublicConfig: {
...ENSINDEXER_PUBLIC_CONFIG,
plugins: ["subgraph"],
isSubgraphCompatible: true,
},
namespace: ENSINDEXER_PUBLIC_CONFIG.namespace,
databaseSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName,
rpcConfigs: new Map(),
customReferralProgramEditionConfigSetUrl: undefined,
theGraphApiKey: "secret-api-key",
};

const result = buildEnsApiPublicConfig(mockConfig);

expect(result.theGraphFallback.canFallback).toBe(true);
// discriminate the type...
if (!result.theGraphFallback.canFallback) throw new Error("never");

// shouldn't have the secret-api-key in the url
expect(result.theGraphFallback.url).not.toMatch(/secret-api-key/gi);
});
});
58 changes: 12 additions & 46 deletions apps/ensapi/src/config/config.schema.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import packageJson from "@/../package.json" with { type: "json" };

import pRetry from "p-retry";
import { parse as parseConnectionString } from "pg-connection-string";
import { version } from "pino";
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
tk-o marked this conversation as resolved.
Outdated
import { prettifyError, ZodError, z } from "zod/v4";

import type { EnsApiPublicConfig } from "@ensnode/ensnode-sdk";
import {
buildRpcConfigsFromEnv,
canFallbackToTheGraph,
DatabaseSchemaNameSchema,
ENSNamespaceSchema,
EnsIndexerUrlSchema,
invariant_rpcConfigsSpecifiedForRootChain,
makeENSIndexerPublicConfigSchema,
makeEnsApiVersionSchema,
OptionalPortNumberSchema,
RpcConfigsSchema,
TheGraphApiKeySchema,
} from "@ensnode/ensnode-sdk/internal";

import { ENSApi_DEFAULT_PORT } from "@/config/defaults";
import type { EnsApiEnvironment } from "@/config/environment";
import { invariant_ensIndexerPublicConfigVersionInfo } from "@/config/validations";
import { fetchENSIndexerConfig } from "@/lib/fetch-ensindexer-config";
import logger from "@/lib/logger";

export const DatabaseUrlSchema = z.string().refine(
Expand Down Expand Up @@ -60,20 +56,21 @@ const CustomReferralProgramEditionConfigSetUrlSchema = z
})
.optional();

const EnsApiVersionSchema = makeEnsApiVersionSchema();

const EnsApiConfigSchema = z

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.

Over the last month we've been solving some details with how the ENSRainbow config logic would work. One of the design choices we made is to try to make it more clear exactly which config is coming from environment variables vs other sources.

Please review how configs are named and built in ENSRainbow.

Applying some of those ideas here, it would mean we should do things including:

  • Rename EnsApiConfig to EnsApiEnvConfig (so that it's explicit how it comes exclusively from environment variables).
  • I suppose then there would be another data model, EnsApiFullConfig (not sure what to call it) that would then be built from both an EnsApiEnvConfig (available synchronously at process startup) and an EnsIndexerPublicConfig (requires to be fetched asynchronously from ENSDb). This EnsApiFullConfig could then be made available synchronously not asynchronously to all the logic in ENSApi via middleware using similar patterns as how the indexing status middleware works.
  • The EnsApiPublicConfig could then be built from an EnsApiFullConfig.

.object({
version: EnsApiVersionSchema,
port: OptionalPortNumberSchema.default(ENSApi_DEFAULT_PORT),
databaseUrl: DatabaseUrlSchema,
databaseSchemaName: DatabaseSchemaNameSchema,
ensIndexerUrl: EnsIndexerUrlSchema,

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.

Can we remove this now?

theGraphApiKey: TheGraphApiKeySchema,
namespace: ENSNamespaceSchema,

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.

It will be great if we can remove this and instead load it dynamically from ENSDb.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll look into how we can replace all reads of config.namespace in apps/ensapi. If we load it from ENSDb, then we'll have to to it in an async way.

rpcConfigs: RpcConfigsSchema,
ensIndexerPublicConfig: makeENSIndexerPublicConfigSchema("ensIndexerPublicConfig"),
customReferralProgramEditionConfigSetUrl: CustomReferralProgramEditionConfigSetUrlSchema,
})
.check(invariant_rpcConfigsSpecifiedForRootChain)
.check(invariant_ensIndexerPublicConfigVersionInfo);
.check(invariant_rpcConfigsSpecifiedForRootChain);

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.

I note how invariants like this couldn't be checked on startup just from environment variables if we remove namespace as an environment variable. However we could move this check into the logic that validates if ENSApi supports a given config it loads from ENSDb.


Comment on lines +58 to 73

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

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

The previous startup-time version compatibility check between ENSApi and the ENSIndexer/ENSDb versions (via invariant_ensIndexerPublicConfigVersionInfo) has been removed, but there doesn’t appear to be an equivalent validation when reading config from ENSDb. This means ENSApi may now run against incompatible ENSDb/ENSIndexer/ENSRainbow versions without failing fast. Consider reintroducing an explicit version-mismatch guard in PublicConfigBuilder.getPublicConfig() (or as a Zod .check in validateEnsApiPublicConfig) to preserve the earlier safety behavior.

Copilot uses AI. Check for mistakes.

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.

Yes, ENSApi should perform maximum validations against the config it loads from ENSDb to ensure it is compatible with it.

export type EnsApiConfig = z.infer<typeof EnsApiConfigSchema>;

Expand All @@ -83,29 +80,19 @@ export type EnsApiConfig = z.infer<typeof EnsApiConfigSchema>;
* @returns A validated EnsApiConfig object
Comment thread
tk-o marked this conversation as resolved.
* @throws Error with formatted validation messages if environment parsing fails
*/
export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promise<EnsApiConfig> {
export function buildConfigFromEnvironment(env: EnsApiEnvironment): EnsApiConfig {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
try {
const ensIndexerUrl = EnsIndexerUrlSchema.parse(env.ENSINDEXER_URL);

const ensIndexerPublicConfig = await pRetry(() => fetchENSIndexerConfig(ensIndexerUrl), {
retries: 3,
onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => {
logger.info(
`ENSIndexer Config fetch attempt ${attemptNumber} failed (${error.message}). ${retriesLeft} retries left.`,
);
},
});

const rpcConfigs = buildRpcConfigsFromEnv(env, ensIndexerPublicConfig.namespace);
const namespace = ENSNamespaceSchema.parse(env.NAMESPACE);
const rpcConfigs = buildRpcConfigsFromEnv(env, namespace);
Comment thread
tk-o marked this conversation as resolved.

return EnsApiConfigSchema.parse({
version: packageJson.version,
port: env.PORT,
databaseUrl: env.DATABASE_URL,
databaseSchemaName: env.DATABASE_SCHEMA,
ensIndexerUrl: env.ENSINDEXER_URL,
theGraphApiKey: env.THEGRAPH_API_KEY,
ensIndexerPublicConfig,
namespace: ensIndexerPublicConfig.namespace,
databaseSchemaName: ensIndexerPublicConfig.databaseSchemaName,
namespace: env.NAMESPACE,
rpcConfigs,
Comment thread
tk-o marked this conversation as resolved.
customReferralProgramEditionConfigSetUrl: env.CUSTOM_REFERRAL_PROGRAM_EDITIONS,
});
Expand All @@ -121,24 +108,3 @@ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promis
process.exit(1);
}
}

/**
* Builds the ENSApi public configuration from an EnsApiConfig object.
*
* @param config - The validated EnsApiConfig object
* @returns A complete ENSApiPublicConfig object
*/
export function buildEnsApiPublicConfig(config: EnsApiConfig): EnsApiPublicConfig {
return {
version: packageJson.version,
theGraphFallback: canFallbackToTheGraph({
namespace: config.namespace,
// NOTE: very important here that we replace the actual server-side api key with a placeholder
// so that it's not sent to clients as part of the `theGraphFallback.url`. The placeholder must
// pass validation, of course, but the only validation necessary is that it is a string.
theGraphApiKey: config.theGraphApiKey ? "<API_KEY>" : undefined,

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.

Maybe I missed it but I didn't see where we retained this logic? Please ignore if it's already retained somewhere.

isSubgraphCompatible: config.ensIndexerPublicConfig.isSubgraphCompatible,
}),
ensIndexerPublicConfig: config.ensIndexerPublicConfig,
};
}
5 changes: 3 additions & 2 deletions apps/ensapi/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import type {
* their state in `process.env`. This interface is intended to be the source type which then gets
* mapped/parsed into a structured configuration object like `EnsApiConfig`.
*/
export type EnsApiEnvironment = Omit<DatabaseEnvironment, "DATABASE_SCHEMA"> &
EnsIndexerUrlEnvironment &
export type EnsApiEnvironment = DatabaseEnvironment & {
NAMESPACE?: string;
Comment thread
vercel[bot] marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.

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.

It would be awesome if we could remove the need for namespace as an env variable and instead make ENSApi fully dynamic based on the config it loads from the database.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Will look into achieving that goal 👍

} & EnsIndexerUrlEnvironment &
RpcEnvironment &
PortEnvironment &
LogLevelEnvironment &
Expand Down
8 changes: 4 additions & 4 deletions apps/ensapi/src/handlers/ensnode-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import {
EnsApiIndexingStatusResponseCodes,
type EnsApiIndexingStatusResponseError,
type EnsApiIndexingStatusResponseOk,
serializeENSApiPublicConfig,
serializeEnsApiIndexingStatusResponse,
serializeEnsApiPublicConfig,
} from "@ensnode/ensnode-sdk";

import { buildEnsApiPublicConfig } from "@/config/config.schema";
import { createApp } from "@/lib/hono-factory";
import { publicConfigBuilder } from "@/lib/public-config-builder/singleton";

import { getConfigRoute, getIndexingStatusRoute } from "./ensnode-api.routes";
import ensnodeGraphQLApi from "./ensnode-graphql-api";
Expand All @@ -20,8 +20,8 @@ import resolutionApi from "./resolution-api";
const app = createApp();

app.openapi(getConfigRoute, async (c) => {
const ensApiPublicConfig = buildEnsApiPublicConfig(config);
return c.json(serializeENSApiPublicConfig(ensApiPublicConfig));
const ensApiPublicConfig = await publicConfigBuilder.getPublicConfig();
return c.json(serializeEnsApiPublicConfig(ensApiPublicConfig));
Comment on lines +21 to +22

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

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

/api/config now awaits publicConfigBuilder.getPublicConfig(), which can throw (e.g., ENSDb not reachable / missing ENSIndexer public config). Right now that exception will bubble to the global onError handler and return a 500. Consider catching expected “not ready/unavailable” failures here and returning a 503 (similar to the /health endpoint) so clients get a more accurate status code.

Suggested change
const ensApiPublicConfig = await publicConfigBuilder.getPublicConfig();
return c.json(serializeEnsApiPublicConfig(ensApiPublicConfig));
try {
const ensApiPublicConfig = await publicConfigBuilder.getPublicConfig();
return c.json(serializeEnsApiPublicConfig(ensApiPublicConfig));
} catch (error) {
return c.json(
{
error: "ENS API public config is not available",
},
503,
);
}

Copilot uses AI. Check for mistakes.

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.

Please see my other related feedback for moving the public config into middleware so that no async operation like this is needed.

});

app.openapi(getIndexingStatusRoute, async (c) => {
Expand Down
12 changes: 7 additions & 5 deletions apps/ensapi/src/handlers/name-tokens-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,13 @@ import {
import { createApp } from "@/lib/hono-factory";
import { findRegisteredNameTokensForDomain } from "@/lib/name-tokens/find-name-tokens-for-domain";
import { getIndexedSubregistries } from "@/lib/name-tokens/get-indexed-subregistries";
import { publicConfigBuilder } from "@/lib/public-config-builder/singleton";
import { nameTokensApiMiddleware } from "@/middleware/name-tokens.middleware";

import { getNameTokensRoute } from "./name-tokens-api.routes";

const app = createApp();

const indexedSubregistries = getIndexedSubregistries(
config.namespace,
config.ensIndexerPublicConfig.plugins as PluginName[],
);

// Middleware managing access to Name Tokens API route.
// It makes the route available if all prerequisites are met,
// and if not returns the appropriate HTTP 503 (Service Unavailable) error.
Expand Down Expand Up @@ -98,6 +94,12 @@ app.openapi(getNameTokensRoute, async (c) => {
}

const parentNode = namehash(getParentNameFQDN(name));
const { ensIndexerPublicConfig } = await publicConfigBuilder.getPublicConfig();

const indexedSubregistries = getIndexedSubregistries(
config.namespace,
ensIndexerPublicConfig.plugins as PluginName[],
);
const subregistry = indexedSubregistries.find((subregistry) => subregistry.node === parentNode);
Comment thread
tk-o marked this conversation as resolved.

// Return 404 response with error code for Name Tokens Not Indexed when
Expand Down
26 changes: 26 additions & 0 deletions apps/ensapi/src/lib/ensdb-client/drizzle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// This file is based on `packages/ponder-subgraph/src/drizzle.ts` file.

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.

Could you please create a follow-up issue for this?

For example we need to work towards refactoring all of the ENS Referrals-related APIs out of ENSApi and into a separate distinct app that would also build on top of ENSDb the same way ENSApi does. We want to make it easy for us and others to build apps on top of ENSDb.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

// We currently duplicate the makeDrizzle function, as we don't have
// a shared package for backend code yet. When we do, we can move
// this function to the shared package and import it in both places.
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" });
};
Loading
Loading