From c5e97bd62bfa80b9634d2e102e32cfdee028387d Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 16:08:16 -0500 Subject: [PATCH 01/15] add pino to ensapi, fix ensrainbow progress logging in tests --- apps/ensapi/package.json | 2 + apps/ensapi/src/config/config.schema.ts | 12 ++- apps/ensapi/src/config/environment.ts | 4 +- apps/ensapi/src/handlers/resolution-api.ts | 76 ++++++++----------- apps/ensapi/src/index.ts | 12 +-- apps/ensapi/src/lib/logger.ts | 33 ++++++++ .../src/lib/resolution/forward-resolution.ts | 3 +- .../middleware/can-accelerate.middleware.ts | 13 ++-- apps/ensapi/vitest.config.ts | 3 + apps/ensrainbow/package.json | 2 +- .../src/commands/convert-command.ts | 1 + .../src/commands/ingest-protobuf-command.ts | 3 +- apps/ensrainbow/src/utils/logger.ts | 17 ++--- apps/ensrainbow/vitest.config.ts | 3 + .../src/shared/config/environments.ts | 7 ++ pnpm-lock.yaml | 16 +++- vitest.config.ts | 3 - 17 files changed, 131 insertions(+), 79 deletions(-) create mode 100644 apps/ensapi/src/lib/logger.ts diff --git a/apps/ensapi/package.json b/apps/ensapi/package.json index 64b70560db..2289740e6e 100644 --- a/apps/ensapi/package.json +++ b/apps/ensapi/package.json @@ -44,6 +44,7 @@ "p-memoize": "^8.0.0", "p-reflect": "^3.1.0", "p-retry": "^7.1.0", + "pino": "^10.1.0", "ponder": "catalog:", "ponder-enrich-gql-docs-middleware": "^0.1.3", "viem": "catalog:", @@ -52,6 +53,7 @@ "devDependencies": { "@ensnode/shared-configs": "workspace:*", "@types/node": "catalog:", + "pino-pretty": "^13.1.2", "tsx": "^4.7.1", "typescript": "catalog:", "vitest": "catalog:" diff --git a/apps/ensapi/src/config/config.schema.ts b/apps/ensapi/src/config/config.schema.ts index c1b3fa40cd..04f9f9ce8e 100644 --- a/apps/ensapi/src/config/config.schema.ts +++ b/apps/ensapi/src/config/config.schema.ts @@ -17,6 +17,7 @@ import { import { ENSApi_DEFAULT_PORT } from "@/config/defaults"; import type { EnsApiEnvironment } from "@/config/environment"; import { invariant_ensIndexerPublicConfigVersionInfo } from "@/config/validations"; +import logger from "@/lib/logger"; const EnsApiConfigSchema = z .object({ @@ -47,7 +48,7 @@ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promis const ensIndexerPublicConfig = await pRetry(() => client.config(), { retries: 3, onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => { - console.log( + logger.info( `ENSIndexer Config fetch attempt ${attemptNumber} failed (${error.message}). ${retriesLeft} retries left.`, ); }, @@ -67,13 +68,16 @@ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promis }); } catch (error) { if (error instanceof ZodError) { - throw new Error(`Failed to parse environment configuration: \n${prettifyError(error)}\n`); + logger.error(`Failed to parse environment configuration: \n${prettifyError(error)}\n`); + process.exit(1); } if (error instanceof Error) { - error.message = `Failed to build EnsApiConfig: ${error.message}`; + logger.error(error, `Failed to build EnsApiConfig`); + process.exit(1); } - throw error; + logger.error(`Unknown Error`); + process.exit(1); } } diff --git a/apps/ensapi/src/config/environment.ts b/apps/ensapi/src/config/environment.ts index d69cbc624a..a02ac785cc 100644 --- a/apps/ensapi/src/config/environment.ts +++ b/apps/ensapi/src/config/environment.ts @@ -1,6 +1,7 @@ import type { DatabaseEnvironment, EnsIndexerUrlEnvironment, + LogLevelEnvironment, PortEnvironment, RpcEnvironment, } from "@ensnode/ensnode-sdk/internal"; @@ -15,4 +16,5 @@ import type { export type EnsApiEnvironment = Omit & EnsIndexerUrlEnvironment & RpcEnvironment & - PortEnvironment; + PortEnvironment & + LogLevelEnvironment; diff --git a/apps/ensapi/src/handlers/resolution-api.ts b/apps/ensapi/src/handlers/resolution-api.ts index 68cad3ffe6..45cc15cde1 100644 --- a/apps/ensapi/src/handlers/resolution-api.ts +++ b/apps/ensapi/src/handlers/resolution-api.ts @@ -6,7 +6,6 @@ import type { ResolveRecordsResponse, } from "@ensnode/ensnode-sdk"; -import { errorResponse } from "@/lib/handlers/error-response"; import { params } from "@/lib/handlers/params.schema"; import { validate } from "@/lib/handlers/validate"; import { factory } from "@/lib/hono-factory"; @@ -51,24 +50,19 @@ app.get( const { selection, trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; - try { - const { result, trace } = await captureTrace(() => - resolveForward(name, selection, { accelerate, canAccelerate }), - ); + const { result, trace } = await captureTrace(() => + resolveForward(name, selection, { accelerate, canAccelerate }), + ); - const response = { - records: result, + const response = { + records: result, - accelerationRequested: accelerate, - accelerationAttempted: accelerate && canAccelerate, - ...(showTrace && { trace }), - } satisfies ResolveRecordsResponse; + accelerationRequested: accelerate, + accelerationAttempted: accelerate && canAccelerate, + ...(showTrace && { trace }), + } satisfies ResolveRecordsResponse; - return c.json(response); - } catch (error) { - console.error(error); - return errorResponse(c, error); - } + return c.json(response); }, ); @@ -99,24 +93,19 @@ app.get( const { trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; - try { - const { result, trace } = await captureTrace(() => - resolveReverse(address, chainId, { accelerate, canAccelerate }), - ); + const { result, trace } = await captureTrace(() => + resolveReverse(address, chainId, { accelerate, canAccelerate }), + ); - const response = { - name: result, + const response = { + name: result, - accelerationRequested: accelerate, - accelerationAttempted: accelerate && canAccelerate, - ...(showTrace && { trace }), - } satisfies ResolvePrimaryNameResponse; + accelerationRequested: accelerate, + accelerationAttempted: accelerate && canAccelerate, + ...(showTrace && { trace }), + } satisfies ResolvePrimaryNameResponse; - return c.json(response); - } catch (error) { - console.error(error); - return errorResponse(c, error); - } + return c.json(response); }, ); @@ -145,24 +134,19 @@ app.get( const { chainIds, trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; - try { - const { result, trace } = await captureTrace(() => - resolvePrimaryNames(address, chainIds, { accelerate, canAccelerate }), - ); + const { result, trace } = await captureTrace(() => + resolvePrimaryNames(address, chainIds, { accelerate, canAccelerate }), + ); - const response = { - names: result, + const response = { + names: result, - accelerationRequested: accelerate, - accelerationAttempted: accelerate && canAccelerate, - ...(showTrace && { trace }), - } satisfies ResolvePrimaryNamesResponse; + accelerationRequested: accelerate, + accelerationAttempted: accelerate && canAccelerate, + ...(showTrace && { trace }), + } satisfies ResolvePrimaryNamesResponse; - return c.json(response); - } catch (error) { - console.error(error); - return errorResponse(c, error); - } + return c.json(response); }, ); diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index c0a547d763..07f1e4e330 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -10,6 +10,7 @@ import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; import { redactEnsApiConfig } from "@/config/redact"; import { errorResponse } from "@/lib/handlers/error-response"; import { factory } from "@/lib/hono-factory"; +import logger from "@/lib/logger"; import { sdk } from "@/lib/tracing/instrumentation"; import { canAccelerateMiddleware } from "@/middleware/can-accelerate.middleware"; import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; @@ -51,7 +52,7 @@ app.get("/health", async (c) => { // log hono errors to console app.onError((error, ctx) => { - console.error(error); + logger.error(error); return errorResponse(ctx, "Internal Server Error"); }); @@ -65,8 +66,9 @@ const server = serve( port: config.port, }, async (info) => { - console.log(`ENSApi listening on port ${info.port} with config:`); - console.log(prettyPrintJson(redactEnsApiConfig(config))); + logger.info( + `ENSApi listening on port ${info.port} with config:\n${prettyPrintJson(redactEnsApiConfig(config))}`, + ); // self-healthcheck to connect to ENSIndexer & warm Indexing Status / Can Accelerate cache await app.request("/health"); @@ -90,7 +92,7 @@ const gracefulShutdown = async () => { process.exit(0); } catch (error) { - console.error(error); + logger.error(error); process.exit(1); } }; @@ -100,6 +102,6 @@ process.on("SIGINT", gracefulShutdown); process.on("SIGTERM", gracefulShutdown); process.on("uncaughtException", async (error) => { - console.error(`Fatal Error:`, error); + logger.error(error, "uncaughtException"); await gracefulShutdown(); }); diff --git a/apps/ensapi/src/lib/logger.ts b/apps/ensapi/src/lib/logger.ts new file mode 100644 index 0000000000..7f64175d40 --- /dev/null +++ b/apps/ensapi/src/lib/logger.ts @@ -0,0 +1,33 @@ +import pino, { type Level, levels } from "pino"; +import { prettifyError, z } from "zod/v4"; + +const makePino = (level: Level) => + pino({ + level, + transport: + process.env.NODE_ENV === "production" + ? undefined + : { + target: "pino-pretty", + options: { + colorize: true, + ignore: "pid,hostname", + }, + }, + }); + +const LogLevelSchema = z + .enum(levels.labels, { + error: `Invalid LOG_LEVEL, expected one of '${Object.values(levels.labels).join("' | '")}'`, + }) + .transform((level) => level as Level) + .default("debug"); + +const level = LogLevelSchema.safeParse(process.env.LOG_LEVEL); + +if (!level.success) { + makePino("fatal").fatal(prettifyError(level.error)); + process.exit(1); +} + +export default makePino(level.data); diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index 18fad49796..afd9742b20 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -19,6 +19,7 @@ import { TraceableENSProtocol, } from "@ensnode/ensnode-sdk"; +import logger from "@/lib/logger"; import { ENS_ROOT_REGISTRY } from "@/lib/protocol-acceleration/ens-root-registry"; import { findResolver } from "@/lib/protocol-acceleration/find-resolver"; import { getENSIP19ReverseNameRecordFromIndex } from "@/lib/protocol-acceleration/get-primary-name-from-index"; @@ -231,7 +232,7 @@ async function _resolveForward( // the selection should just be `{ name: true }`, but technically not prohibited to // select more records than just 'name', so just warn if that happens. if (selection.addresses !== undefined || selection.texts !== undefined) { - console.warn( + logger.warn( `Sanity Check(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected a selection of exactly '{ name: true }' but received ${JSON.stringify(selection)}.`, ); } diff --git a/apps/ensapi/src/middleware/can-accelerate.middleware.ts b/apps/ensapi/src/middleware/can-accelerate.middleware.ts index 8ba1719b16..993cfdd4b9 100644 --- a/apps/ensapi/src/middleware/can-accelerate.middleware.ts +++ b/apps/ensapi/src/middleware/can-accelerate.middleware.ts @@ -13,6 +13,7 @@ import { } from "@ensnode/ensnode-sdk"; import { factory } from "@/lib/hono-factory"; +import logger from "@/lib/logger"; export type CanAccelerateVariables = { canAccelerate: boolean }; @@ -62,7 +63,7 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) // log one warning to the console if !hasProtocolAccelerationPlugin if (!didWarnNoProtocolAccelerationPlugin && !hasProtocolAccelerationPlugin) { - console.warn( + logger.warn( `ENSApi is connected to an ENSIndexer that does NOT include the ${PluginName.ProtocolAcceleration} plugin: ENSApi will NOT be able to accelerate Resolution API requests, even if ?accelerate=true. Resolution requests will abide by the full Forward/Reverse Resolution specification, including RPC calls and CCIP-Read requests to external CCIP-Read Gateways.`, ); @@ -81,7 +82,7 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) (!didInitialIndexingStatus && indexingStatusOk) || // first time (didInitialIndexingStatus && !prevIndexingStatusOk && indexingStatusOk) // future change in status ) { - console.log(`ENSIndexer Indexing Status: AVAILABLE`); + logger.info(`ENSIndexer Indexing Status: AVAILABLE`); } // log notice with reason when Indexing Status is unavilable @@ -90,11 +91,11 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) (didInitialIndexingStatus && prevIndexingStatusOk && !indexingStatusOk) // future change in status ) { if (c.var.indexingStatus.isRejected) { - console.warn( + logger.warn( `ENSIndexer Indexing Status: UNAVAILABLE. ENSApi was unable to fetch the current ENSIndexer Indexing Status: ${c.var.indexingStatus.reason}`, ); } else if (c.var.indexingStatus.value.responseCode === IndexingStatusResponseCodes.Error) { - console.warn( + logger.warn( `ENSIndexer Indexing Status: UNAVAILABLE. ENSIndexer is reporting an Indexing Status Error.`, ); } @@ -126,7 +127,7 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) (!didInitialRealtime && isWithinMaxRealtime) || // first time (didInitialRealtime && !prevIsWithinMaxRealtime && isWithinMaxRealtime) // future change in status ) { - console.log(`ENSIndexer is realtime, Protocol Acceleration is now ENABLED.`); + logger.info(`ENSIndexer is realtime, Protocol Acceleration is now ENABLED.`); } // log notice when ENSIndexer transitions out of realtime @@ -134,7 +135,7 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) (!didInitialRealtime && !isWithinMaxRealtime) || // first time (didInitialRealtime && prevIsWithinMaxRealtime && !isWithinMaxRealtime) // future change in status ) { - console.warn( + logger.warn( `ENSIndexer is NOT realtime (Worst Case Lag: ${c.var.indexingStatus.value.realtimeProjection.worstCaseDistance} seconds > ${MAX_REALTIME_DISTANCE_TO_ACCELERATE} seconds), Protocol Acceleration is currently DISABLED.`, ); } diff --git a/apps/ensapi/vitest.config.ts b/apps/ensapi/vitest.config.ts index 3428a80d83..f8d6e771c9 100644 --- a/apps/ensapi/vitest.config.ts +++ b/apps/ensapi/vitest.config.ts @@ -10,5 +10,8 @@ export default defineProject({ }, test: { environment: "node", + env: { + LOG_LEVEL: "fatal", + }, }, }); diff --git a/apps/ensrainbow/package.json b/apps/ensrainbow/package.json index 6e0b031662..01ad96eb95 100644 --- a/apps/ensrainbow/package.json +++ b/apps/ensrainbow/package.json @@ -35,7 +35,6 @@ "classic-level": "^1.4.1", "hono": "catalog:", "pino": "^10.1.0", - "pino-pretty": "^13.1.2", "progress": "^2.0.3", "protobufjs": "^7.4.0", "viem": "catalog:", @@ -46,6 +45,7 @@ "@types/node": "^20.17.14", "@types/progress": "^2.0.7", "@types/yargs": "^17.0.32", + "pino-pretty": "^13.1.2", "tsx": "^4.19.3", "typescript": "^5.3.3", "vitest": "catalog:" diff --git a/apps/ensrainbow/src/commands/convert-command.ts b/apps/ensrainbow/src/commands/convert-command.ts index dde51af7fa..964026aafc 100644 --- a/apps/ensrainbow/src/commands/convert-command.ts +++ b/apps/ensrainbow/src/commands/convert-command.ts @@ -50,6 +50,7 @@ function setupProgressBar(): ProgressBar { incomplete: " ", width: 40, total: 150000000, // estimated + stream: logger.level === "silent" ? createWriteStream("/dev/null") : undefined, }, ); } diff --git a/apps/ensrainbow/src/commands/ingest-protobuf-command.ts b/apps/ensrainbow/src/commands/ingest-protobuf-command.ts index 2f3b07277f..5e0a97d7da 100644 --- a/apps/ensrainbow/src/commands/ingest-protobuf-command.ts +++ b/apps/ensrainbow/src/commands/ingest-protobuf-command.ts @@ -1,4 +1,4 @@ -import { createReadStream } from "node:fs"; +import { createReadStream, createWriteStream } from "node:fs"; import ProgressBar from "progress"; import protobuf from "protobufjs"; @@ -105,6 +105,7 @@ export async function ingestProtobufCommand(options: IngestProtobufCommandOption incomplete: " ", width: 40, total: 1000000000, // Placeholder total + stream: logger.level === "silent" ? createWriteStream("/dev/null") : undefined, }, ); diff --git a/apps/ensrainbow/src/utils/logger.ts b/apps/ensrainbow/src/utils/logger.ts index 21e7a0f17e..d6313de784 100644 --- a/apps/ensrainbow/src/utils/logger.ts +++ b/apps/ensrainbow/src/utils/logger.ts @@ -50,18 +50,15 @@ export function createLogger(level: LogLevel = DEFAULT_LOG_LEVEL): pino.Logger { return pino({ level, - ...(isProduction - ? {} // In production, use default pino output format + transport: isProduction + ? undefined : { - transport: { - target: "pino-pretty", - options: { - colorize: true, - translateTime: "HH:MM:ss", - ignore: "pid,hostname", - }, + target: "pino-pretty", + options: { + colorize: true, + ignore: "pid,hostname", }, - }), + }, }); } diff --git a/apps/ensrainbow/vitest.config.ts b/apps/ensrainbow/vitest.config.ts index 3428a80d83..918d3a3fa9 100644 --- a/apps/ensrainbow/vitest.config.ts +++ b/apps/ensrainbow/vitest.config.ts @@ -10,5 +10,8 @@ export default defineProject({ }, test: { environment: "node", + env: { + LOG_LEVEL: "silent", + }, }, }); diff --git a/packages/ensnode-sdk/src/shared/config/environments.ts b/packages/ensnode-sdk/src/shared/config/environments.ts index c4b3796e68..e9bd00e70a 100644 --- a/packages/ensnode-sdk/src/shared/config/environments.ts +++ b/packages/ensnode-sdk/src/shared/config/environments.ts @@ -35,3 +35,10 @@ export interface PortEnvironment { * May contain a comma separated list of one or more URLs. */ export type ChainIdSpecificRpcEnvironmentVariable = string; + +/** + * Environment variables for log level configuration. + */ +export type LogLevelEnvironment = { + LOG_LEVEL?: string; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3eb961ff7b..439cea9964 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -333,6 +333,9 @@ importers: p-retry: specifier: ^7.1.0 version: 7.1.0 + pino: + specifier: 10.1.0 + version: 10.1.0 ponder: specifier: 'catalog:' version: 0.13.14(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.15.3)(bufferutil@4.0.9)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(typescript@5.7.3)(utf-8-validate@5.0.10)(viem@2.23.2(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(zod@3.25.76))(yaml@2.7.0)(zod@3.25.76) @@ -352,6 +355,9 @@ importers: '@types/node': specifier: 'catalog:' version: 22.15.3 + pino-pretty: + specifier: ^13.1.2 + version: 13.1.2 tsx: specifier: ^4.7.1 version: 4.19.3 @@ -11147,6 +11153,14 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 + '@vitest/mocker@4.0.3(vite@7.1.11(@types/node@20.17.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0))': + dependencies: + '@vitest/spy': 4.0.3 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 7.1.11(@types/node@20.17.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0) + '@vitest/mocker@4.0.3(vite@7.1.11(@types/node@22.15.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@vitest/spy': 4.0.3 @@ -15754,7 +15768,7 @@ snapshots: vitest@4.0.3(@types/debug@4.1.12)(@types/node@20.17.14)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0): dependencies: '@vitest/expect': 4.0.3 - '@vitest/mocker': 4.0.3(vite@7.1.11(@types/node@22.15.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0)) + '@vitest/mocker': 4.0.3(vite@7.1.11(@types/node@20.17.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0)) '@vitest/pretty-format': 4.0.3 '@vitest/runner': 4.0.3 '@vitest/snapshot': 4.0.3 diff --git a/vitest.config.ts b/vitest.config.ts index d8873fd84b..5fc0bdd08a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,8 +3,5 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { projects: ["apps/*/vitest.config.ts", "packages/*/vitest.config.ts"], - env: { - LOG_LEVEL: "silent", - }, }, }); From 0a303c82bd5acf7060d547ca575d0719df5bd515 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 16:09:18 -0500 Subject: [PATCH 02/15] fix: frozen lockfile --- pnpm-lock.yaml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 439cea9964..4725233fe0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -449,9 +449,6 @@ importers: pino: specifier: 10.1.0 version: 10.1.0 - pino-pretty: - specifier: ^13.1.2 - version: 13.1.2 progress: specifier: ^2.0.3 version: 2.0.3 @@ -477,6 +474,9 @@ importers: '@types/yargs': specifier: ^17.0.32 version: 17.0.33 + pino-pretty: + specifier: ^13.1.2 + version: 13.1.2 tsx: specifier: ^4.19.3 version: 4.19.3 @@ -11153,14 +11153,6 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.3(vite@7.1.11(@types/node@20.17.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0))': - dependencies: - '@vitest/spy': 4.0.3 - estree-walker: 3.0.3 - magic-string: 0.30.19 - optionalDependencies: - vite: 7.1.11(@types/node@20.17.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0) - '@vitest/mocker@4.0.3(vite@7.1.11(@types/node@22.15.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@vitest/spy': 4.0.3 @@ -15768,7 +15760,7 @@ snapshots: vitest@4.0.3(@types/debug@4.1.12)(@types/node@20.17.14)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0): dependencies: '@vitest/expect': 4.0.3 - '@vitest/mocker': 4.0.3(vite@7.1.11(@types/node@20.17.14)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0)) + '@vitest/mocker': 4.0.3(vite@7.1.11(@types/node@22.15.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(yaml@2.7.0)) '@vitest/pretty-format': 4.0.3 '@vitest/runner': 4.0.3 '@vitest/snapshot': 4.0.3 From 834348e9fe03876ac6eb9e3221a4cab65d765d00 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 16:44:48 -0500 Subject: [PATCH 03/15] checkpoint: integrate ensapipublicconfig into ensadmin --- .../app/mock/recent-registrations/page.tsx | 9 +- .../connection/config-info/config-info.tsx | 78 +++++++++------- .../src/components/connection/index.tsx | 28 +++--- .../connections-library-selector.tsx | 2 +- .../connections/require-active-connection.tsx | 4 +- .../recent-registrations/components.tsx | 9 +- .../recent-registrations/registrations.tsx | 43 ++------- .../hooks/active/use-active-connection.tsx | 4 +- .../src/hooks/active/use-active-namespace.ts | 2 +- .../ensadmin/src/hooks/async/use-namespace.ts | 6 +- apps/ensapi/src/config/config.schema.test.ts | 60 ++++++++++++- apps/ensapi/src/config/config.schema.ts | 21 ++++- apps/ensapi/src/handlers/ensnode-api.ts | 8 +- .../ensapi/src/lib/fetch-ensindexer-config.ts | 17 ++++ packages/ensnode-react/src/hooks/index.ts | 2 +- ...exerConfig.ts => useENSNodeConfigQuery.ts} | 10 +-- packages/ensnode-react/src/utils/query.ts | 6 +- packages/ensnode-sdk/src/api/types.ts | 5 +- packages/ensnode-sdk/src/client.test.ts | 56 ++++++------ packages/ensnode-sdk/src/client.ts | 16 ++-- .../src/ensapi/config/conversions.test.ts | 88 +++++++++++++++++++ .../src/ensapi/config/deserialize.ts | 24 +++++ .../ensnode-sdk/src/ensapi/config/index.ts | 5 ++ .../src/ensapi/config/serialize.ts | 17 ++++ .../src/ensapi/config/serialized-types.ts | 13 +++ .../ensnode-sdk/src/ensapi/config/types.ts | 24 +++++ .../src/ensapi/config/zod-schemas.ts | 17 ++++ packages/ensnode-sdk/src/ensapi/index.ts | 1 + packages/ensnode-sdk/src/index.ts | 1 + 29 files changed, 418 insertions(+), 158 deletions(-) create mode 100644 apps/ensapi/src/lib/fetch-ensindexer-config.ts rename packages/ensnode-react/src/hooks/{useENSIndexerConfig.ts => useENSNodeConfigQuery.ts} (60%) create mode 100644 packages/ensnode-sdk/src/ensapi/config/conversions.test.ts create mode 100644 packages/ensnode-sdk/src/ensapi/config/deserialize.ts create mode 100644 packages/ensnode-sdk/src/ensapi/config/index.ts create mode 100644 packages/ensnode-sdk/src/ensapi/config/serialize.ts create mode 100644 packages/ensnode-sdk/src/ensapi/config/serialized-types.ts create mode 100644 packages/ensnode-sdk/src/ensapi/config/types.ts create mode 100644 packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts create mode 100644 packages/ensnode-sdk/src/ensapi/index.ts diff --git a/apps/ensadmin/src/app/mock/recent-registrations/page.tsx b/apps/ensadmin/src/app/mock/recent-registrations/page.tsx index f690213b34..fcc711102e 100644 --- a/apps/ensadmin/src/app/mock/recent-registrations/page.tsx +++ b/apps/ensadmin/src/app/mock/recent-registrations/page.tsx @@ -16,7 +16,6 @@ import { import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { ensIndexerPublicConfig } from "../config-api.mock"; import { indexingStatusResponseOkOmnichain } from "../indexing-status-api.mock"; type LoadingVariant = "Loading" | "Loading Error"; @@ -39,7 +38,7 @@ export default function MockRegistrationsPage() { return { error: { title: "RecentRegistrations Error", - description: "Failed to fetch ENSIndexerConfig or IndexingStatus.", + description: "Failed to fetch IndexingStatus.", }, } satisfies RecentRegistrationsErrorProps; @@ -61,7 +60,6 @@ export default function MockRegistrationsPage() { } return { - ensIndexerConfig: ensIndexerPublicConfig, realtimeProjection: indexingStatus.realtimeProjection, } satisfies RecentRegistrationsOkProps; } catch (error) { @@ -113,10 +111,7 @@ export default function MockRegistrationsPage() { {typeof props.error !== "undefined" ? ( ) : ( - + )} ); diff --git a/apps/ensadmin/src/components/connection/config-info/config-info.tsx b/apps/ensadmin/src/components/connection/config-info/config-info.tsx index 315ab87000..4ac1cd2318 100644 --- a/apps/ensadmin/src/components/connection/config-info/config-info.tsx +++ b/apps/ensadmin/src/components/connection/config-info/config-info.tsx @@ -7,7 +7,7 @@ import { PlugZap, Replace } from "lucide-react"; -import type { ENSIndexerPublicConfig } from "@ensnode/ensnode-sdk"; +import type { ENSApiPublicConfig, ENSIndexerPublicConfig } from "@ensnode/ensnode-sdk"; import { ChainIcon } from "@/components/chains/ChainIcon"; import { ConfigInfoAppCard } from "@/components/connection/config-info/app-card"; @@ -31,20 +31,20 @@ import { cn } from "@/lib/utils"; /** * ENSNodeConfigInfo display variations: * - * Standard - ensIndexerConfig: ENSIndexerPublicConfig, error: undefined - * Loading - ensIndexerConfig: undefined, error: undefined - * Error - ensIndexerConfig: undefined, error: ErrorInfoProps + * Standard - ensApiPublicConfig: ENSIndexerPublicConfig, error: undefined + * Loading - ensApiPublicConfig: undefined, error: undefined + * Error - ensApiPublicConfig: undefined, error: ErrorInfoProps * - * @throws If both ensIndexerConfig and error are defined + * @throws If both ensApiPublicConfig and error are defined */ export interface ENSNodeConfigProps { - ensIndexerConfig?: ENSIndexerPublicConfig; + ensApiPublicConfig?: ENSApiPublicConfig; error?: ErrorInfoProps; ensAdminVersion?: string; } export function ENSNodeConfigInfo({ - ensIndexerConfig, + ensApiPublicConfig, error, ensAdminVersion, }: ENSNodeConfigProps) { @@ -53,22 +53,24 @@ export function ENSNodeConfigInfo({ const cardItemValueStyles = "text-sm leading-6 font-normal text-black"; const { rawSelectedConnection } = useSelectedConnection(); - if (error !== undefined && ensIndexerConfig !== undefined) { - throw new Error("Invariant: ENSNodeConfigInfo with both ensIndexerConfig and error defined."); + if (error !== undefined && ensApiPublicConfig !== undefined) { + throw new Error("Invariant: ENSNodeConfigInfo with both ensApiPublicConfig and error defined."); } if (error !== undefined) { return ; } - if (ensIndexerConfig === undefined) { + if (ensApiPublicConfig === undefined) { return ; } - const healReverseAddressesActivated = !ensIndexerConfig.isSubgraphCompatible; - const indexAdditionalRecordsActivated = !ensIndexerConfig.isSubgraphCompatible; - const replaceUnnormalizedLabelsActivated = !ensIndexerConfig.isSubgraphCompatible; - const subgraphCompatibilityActivated = ensIndexerConfig.isSubgraphCompatible; + const { ensIndexerPublicConfig } = ensApiPublicConfig; + + const healReverseAddressesActivated = !ensIndexerPublicConfig.isSubgraphCompatible; + const indexAdditionalRecordsActivated = !ensIndexerPublicConfig.isSubgraphCompatible; + const replaceUnnormalizedLabelsActivated = !ensIndexerPublicConfig.isSubgraphCompatible; + const subgraphCompatibilityActivated = ensIndexerPublicConfig.isSubgraphCompatible; const healReverseAddressesDescription = healReverseAddressesActivated ? (

Subnames of addr.reverse will all be known (healed) labels.

@@ -156,6 +158,14 @@ export function ENSNodeConfigInfo({
+ {/*ENSApi*/} + } + version={ensApiPublicConfig.version} + docsLink={new URL("https://ensnode.io/ensapi/")} + /> + {/*ENSDb*/} {ensIndexerConfig.databaseSchemaName}

+

+ {ensIndexerPublicConfig.databaseSchemaName} +

), additionalInfo: (

@@ -177,7 +189,7 @@ export function ENSNodeConfigInfo({ ), }, ]} - version={ensIndexerConfig.versionInfo.ensDb} + version={ensIndexerPublicConfig.versionInfo.ensDb} docsLink={new URL("https://ensnode.io/ensdb/")} /> @@ -189,13 +201,15 @@ export function ENSNodeConfigInfo({ { label: "Node.js", value: ( -

{ensIndexerConfig.versionInfo.nodejs}

+

+ {ensIndexerPublicConfig.versionInfo.nodejs} +

), additionalInfo: (

Version of the{" "} Node.js {" "} @@ -206,13 +220,15 @@ export function ENSNodeConfigInfo({ { label: "Ponder", value: ( -

{ensIndexerConfig.versionInfo.ponder}

+

+ {ensIndexerPublicConfig.versionInfo.ponder} +

), additionalInfo: (

Version of the{" "} ponder {" "} @@ -224,14 +240,14 @@ export function ENSNodeConfigInfo({ label: "ens-normalize.js", value: (

- {ensIndexerConfig.versionInfo.ensNormalize} + {ensIndexerPublicConfig.versionInfo.ensNormalize}

), additionalInfo: (

Version of the{" "} @adraffy/ens-normalize {" "} @@ -244,8 +260,8 @@ export function ENSNodeConfigInfo({ value: (

  • - {ensIndexerConfig.labelSet.labelSetId}: - {ensIndexerConfig.labelSet.labelSetVersion} + {ensIndexerPublicConfig.labelSet.labelSetId}: + {ensIndexerPublicConfig.labelSet.labelSetVersion}
), @@ -264,14 +280,14 @@ export function ENSNodeConfigInfo({ }, { label: "ENS Namespace", - value:

{ensIndexerConfig.namespace}

, + value:

{ensIndexerPublicConfig.namespace}

, additionalInfo:

The ENS namespace that ENSNode operates in the context of.

, }, { label: "Indexed Chains", value: (
- {Array.from(ensIndexerConfig.indexedChainIds).map((chainId) => ( + {Array.from(ensIndexerPublicConfig.indexedChainIds).map((chainId) => ( @@ -291,7 +307,7 @@ export function ENSNodeConfigInfo({ label: "Plugins", value: (
- {ensIndexerConfig.plugins.map((plugin) => ( + {ensIndexerPublicConfig.plugins.map((plugin) => ( @@ -352,8 +368,8 @@ export function ENSNodeConfigInfo({ label: "Server LabelSet", value: (

- {ensIndexerConfig.labelSet.labelSetId}: - {ensIndexerConfig.labelSet.labelSetVersion} + {ensIndexerPublicConfig.labelSet.labelSetId}: + {ensIndexerPublicConfig.labelSet.labelSetVersion}

), additionalInfo: ( @@ -368,7 +384,7 @@ export function ENSNodeConfigInfo({ ), }, ]} - version={ensIndexerConfig.versionInfo.ensRainbow} + version={ensIndexerPublicConfig.versionInfo.ensRainbow} docsLink={new URL("https://ensnode.io/ensrainbow/")} />
diff --git a/apps/ensadmin/src/components/connection/index.tsx b/apps/ensadmin/src/components/connection/index.tsx index edf1a56d16..a92150cdd1 100644 --- a/apps/ensadmin/src/components/connection/index.tsx +++ b/apps/ensadmin/src/components/connection/index.tsx @@ -2,7 +2,7 @@ import { use } from "react"; -import { useENSIndexerConfig } from "@ensnode/ensnode-react"; +import { useENSNodeConfigQuery } from "@ensnode/ensnode-react"; import { ENSNodeConfigInfo } from "@/components/connection/config-info"; import { ensAdminVersion } from "@/lib/env"; @@ -11,33 +11,31 @@ const versionPromise = ensAdminVersion(); export default function ConnectionInfo() { const version = use(versionPromise); - const ensIndexerConfigQuery = useENSIndexerConfig(); + const { status, error, data } = useENSNodeConfigQuery(); - if (ensIndexerConfigQuery.isError) { + if (status === "pending") { + return ( +
+ +
+ ); + } + + if (status === "error") { return ( ); } - if (!ensIndexerConfigQuery.isSuccess) { - return ( -
- {/*display loading state*/} -
- ); - } - - const ensIndexerConfig = ensIndexerConfigQuery.data; - return (
- +
); } diff --git a/apps/ensadmin/src/components/connections/connections-library-selector.tsx b/apps/ensadmin/src/components/connections/connections-library-selector.tsx index be9fe64e3c..7eb69b71af 100644 --- a/apps/ensadmin/src/components/connections/connections-library-selector.tsx +++ b/apps/ensadmin/src/components/connections/connections-library-selector.tsx @@ -73,7 +73,7 @@ export function ConnectionsLibrarySelector() { } else if (!selectedConnection.validatedSelectedConnection.isValid) { connectionMessage = "Invalid connection"; } else { - connectionMessage = "Select ENSNode"; + connectionMessage = selectedConnection.validatedSelectedConnection.url.href; } const serverConnections = connectionLibrary.filter((connection) => connection.type === "server"); diff --git a/apps/ensadmin/src/components/connections/require-active-connection.tsx b/apps/ensadmin/src/components/connections/require-active-connection.tsx index 5f0190f76b..c95f235cde 100644 --- a/apps/ensadmin/src/components/connections/require-active-connection.tsx +++ b/apps/ensadmin/src/components/connections/require-active-connection.tsx @@ -2,7 +2,7 @@ import type { PropsWithChildren } from "react"; -import { useENSIndexerConfig } from "@ensnode/ensnode-react"; +import { useENSNodeConfigQuery } from "@ensnode/ensnode-react"; import { ErrorInfo } from "@/components/error-info"; import { LoadingSpinner } from "@/components/loading-spinner"; @@ -11,7 +11,7 @@ import { LoadingSpinner } from "@/components/loading-spinner"; * Allows consumers to use `useActiveConnection` by blocking rendering until it is available. */ export function RequireActiveConnection({ children }: PropsWithChildren) { - const { status, error } = useENSIndexerConfig(); + const { status, error } = useENSNodeConfigQuery(); if (status === "pending") return ; diff --git a/apps/ensadmin/src/components/recent-registrations/components.tsx b/apps/ensadmin/src/components/recent-registrations/components.tsx index 4058e950bd..acbf7c0ade 100644 --- a/apps/ensadmin/src/components/recent-registrations/components.tsx +++ b/apps/ensadmin/src/components/recent-registrations/components.tsx @@ -5,7 +5,6 @@ import Link from "next/link"; import { Fragment } from "react"; import { - type ENSIndexerPublicConfig, type OmnichainIndexingStatusId, OmnichainIndexingStatusIds, type OmnichainIndexingStatusSnapshot, @@ -41,7 +40,6 @@ const SUPPORTED_OMNICHAIN_INDEXING_STATUSES: OmnichainIndexingStatusId[] = [ ]; export interface RecentRegistrationsOkProps { - ensIndexerConfig: ENSIndexerPublicConfig | undefined; realtimeProjection: RealtimeIndexingStatusProjection | undefined; maxRecords?: number; } @@ -54,18 +52,15 @@ export interface RecentRegistrationsErrorProps { * RecentRegistrations display variations: * * Standard - - * ensIndexerConfig: {@link ENSIndexerPublicConfig}, * indexingStatus: {@link OmnichainIndexingStatusSnapshotCompleted} | * {@link OmnichainIndexingStatusSnapshotFollowing}, * * UnsupportedOmnichainIndexingStatusMessage - - * ensIndexerConfig: {@link ENSIndexerPublicConfig}, * indexingStatus: {@link OmnichainIndexingStatusSnapshot} other than * {@link OmnichainIndexingStatusSnapshotCompleted} | * {@link OmnichainIndexingStatusSnapshotFollowing}, * * Loading - - * ensIndexerConfig: undefined, * indexingStatus: undefined, * * Error - @@ -84,9 +79,9 @@ export function RecentRegistrations(props: RecentRegistrationsProps) { return ; } - const { ensIndexerConfig, realtimeProjection, maxRecords = DEFAULT_MAX_RECORDS } = props; + const { realtimeProjection, maxRecords = DEFAULT_MAX_RECORDS } = props; - if (ensIndexerConfig === undefined || realtimeProjection === undefined) { + if (realtimeProjection === undefined) { return ; } diff --git a/apps/ensadmin/src/components/recent-registrations/registrations.tsx b/apps/ensadmin/src/components/recent-registrations/registrations.tsx index dfbf02048c..2e3f406678 100644 --- a/apps/ensadmin/src/components/recent-registrations/registrations.tsx +++ b/apps/ensadmin/src/components/recent-registrations/registrations.tsx @@ -1,45 +1,27 @@ "use client"; -import { useENSIndexerConfig, useIndexingStatus } from "@ensnode/ensnode-react"; +import { useIndexingStatus } from "@ensnode/ensnode-react"; import { IndexingStatusResponseCodes } from "@ensnode/ensnode-sdk"; import { RecentRegistrations } from "@/components/recent-registrations/components"; export function Registrations() { - const ensIndexerConfigQuery = useENSIndexerConfig(); - const indexingStatusQuery = useIndexingStatus(); + const { status, data: indexingStatus, error } = useIndexingStatus(); - if (ensIndexerConfigQuery.isError) { + if (status === "pending") { return ( - - ); - } - - if (indexingStatusQuery.isError) { - return ( - +
+ +
); } - if (!ensIndexerConfigQuery.isSuccess || !indexingStatusQuery.isSuccess) { + if (status === "error") { return ( -
- {" "} - {/*display loading state*/} -
+ ); } - const ensIndexerConfig = ensIndexerConfigQuery.data; - const indexingStatus = indexingStatusQuery.data; - // even though indexing status was fetched successfully, // it can still refer to a server-side error if (indexingStatus.responseCode === IndexingStatusResponseCodes.Error) { @@ -53,12 +35,5 @@ export function Registrations() { ); } - return ( -
- -
- ); + return ; } diff --git a/apps/ensadmin/src/hooks/active/use-active-connection.tsx b/apps/ensadmin/src/hooks/active/use-active-connection.tsx index c6358ac0e8..fd40f00f08 100644 --- a/apps/ensadmin/src/hooks/active/use-active-connection.tsx +++ b/apps/ensadmin/src/hooks/active/use-active-connection.tsx @@ -1,6 +1,6 @@ "use client"; -import { useENSIndexerConfig } from "@ensnode/ensnode-react"; +import { useENSNodeConfigQuery } from "@ensnode/ensnode-react"; /** * Hook to get the currently active ENSNode connection synchronously. @@ -16,7 +16,7 @@ import { useENSIndexerConfig } from "@ensnode/ensnode-react"; * @throws Error if no active ENSNode connection is available */ export function useActiveConnection() { - const { data } = useENSIndexerConfig(); + const { data } = useENSNodeConfigQuery(); if (data === undefined) { throw new Error(`Invariant(useActiveConnection): Expected an active ENSNode Config`); diff --git a/apps/ensadmin/src/hooks/active/use-active-namespace.ts b/apps/ensadmin/src/hooks/active/use-active-namespace.ts index 9acbe53f1a..486417e7c8 100644 --- a/apps/ensadmin/src/hooks/active/use-active-namespace.ts +++ b/apps/ensadmin/src/hooks/active/use-active-namespace.ts @@ -13,4 +13,4 @@ import { useActiveConnection } from "./use-active-connection"; * @returns The namespace from the active ENSNode configuration * @throws Error if no active ENSNode Config is available */ -export const useActiveNamespace = () => useActiveConnection().namespace; +export const useActiveNamespace = () => useActiveConnection().ensIndexerPublicConfig.namespace; diff --git a/apps/ensadmin/src/hooks/async/use-namespace.ts b/apps/ensadmin/src/hooks/async/use-namespace.ts index 952ae0a3bd..0e04e1cdc7 100644 --- a/apps/ensadmin/src/hooks/async/use-namespace.ts +++ b/apps/ensadmin/src/hooks/async/use-namespace.ts @@ -1,4 +1,4 @@ -import { useENSIndexerConfig } from "@ensnode/ensnode-react"; +import { useENSNodeConfigQuery } from "@ensnode/ensnode-react"; /** * Hook to get the namespace ID from the active ENSNode connection. @@ -22,10 +22,10 @@ import { useENSIndexerConfig } from "@ensnode/ensnode-react"; * ``` */ export function useNamespace() { - const query = useENSIndexerConfig(); + const query = useENSNodeConfigQuery(); return { ...query, - data: query.data?.namespace ?? null, + data: query.data?.ensIndexerPublicConfig.namespace ?? null, }; } diff --git a/apps/ensapi/src/config/config.schema.test.ts b/apps/ensapi/src/config/config.schema.test.ts index 721a2de00c..7ca3b42bdd 100644 --- a/apps/ensapi/src/config/config.schema.test.ts +++ b/apps/ensapi/src/config/config.schema.test.ts @@ -9,7 +9,7 @@ import { } from "@ensnode/ensnode-sdk"; import type { RpcConfig } from "@ensnode/ensnode-sdk/internal"; -import { buildConfigFromEnvironment } from "@/config/config.schema"; +import { buildConfigFromEnvironment, buildEnsApiPublicConfig } from "@/config/config.schema"; import { ENSApi_DEFAULT_PORT } from "@/config/defaults"; import type { EnsApiEnvironment } from "@/config/environment"; @@ -73,3 +73,61 @@ describe("buildConfigFromEnvironment", () => { }); }); }); + +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, + ], + ]), + }; + + const result = buildEnsApiPublicConfig(mockConfig); + + expect(result).toStrictEqual({ + version: packageJson.version, + 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(), + }; + + 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, + ); + }); +}); diff --git a/apps/ensapi/src/config/config.schema.ts b/apps/ensapi/src/config/config.schema.ts index 04f9f9ce8e..c5e1bfe39b 100644 --- a/apps/ensapi/src/config/config.schema.ts +++ b/apps/ensapi/src/config/config.schema.ts @@ -1,7 +1,9 @@ +import packageJson from "@/../package.json" with { type: "json" }; + import pRetry from "p-retry"; import { prettifyError, ZodError, z } from "zod/v4"; -import { ENSNodeClient, serializeENSIndexerPublicConfig } from "@ensnode/ensnode-sdk"; +import { type ENSApiPublicConfig, serializeENSIndexerPublicConfig } from "@ensnode/ensnode-sdk"; import { buildRpcConfigsFromEnv, DatabaseSchemaNameSchema, @@ -17,6 +19,7 @@ import { 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"; const EnsApiConfigSchema = z @@ -43,9 +46,8 @@ export type EnsApiConfig = z.infer; export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promise { try { const ensIndexerUrl = EnsIndexerUrlSchema.parse(env.ENSINDEXER_URL); - const client = new ENSNodeClient({ url: ensIndexerUrl }); - const ensIndexerPublicConfig = await pRetry(() => client.config(), { + const ensIndexerPublicConfig = await pRetry(() => fetchENSIndexerConfig(ensIndexerUrl), { retries: 3, onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => { logger.info( @@ -81,3 +83,16 @@ 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, + ensIndexerPublicConfig: config.ensIndexerPublicConfig, + }; +} diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index c858675971..2fd9ad89c5 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -3,19 +3,21 @@ import config from "@/config"; import { IndexingStatusResponseCodes, type IndexingStatusResponseError, - serializeENSIndexerPublicConfig, + serializeENSApiPublicConfig, serializeIndexingStatusResponse, } from "@ensnode/ensnode-sdk"; +import { buildEnsApiPublicConfig } from "@/config/config.schema"; import { factory } from "@/lib/hono-factory"; import resolutionApi from "./resolution-api"; const app = factory.createApp(); -// include ENSIndexer Public Config endpoint +// include ENSApi Public Config endpoint app.get("/config", async (c) => { - return c.json(serializeENSIndexerPublicConfig(config.ensIndexerPublicConfig)); + const ensApiPublicConfig = buildEnsApiPublicConfig(config); + return c.json(serializeENSApiPublicConfig(ensApiPublicConfig)); }); // include ENSIndexer Indexing Status endpoint diff --git a/apps/ensapi/src/lib/fetch-ensindexer-config.ts b/apps/ensapi/src/lib/fetch-ensindexer-config.ts new file mode 100644 index 0000000000..0cd11beb8b --- /dev/null +++ b/apps/ensapi/src/lib/fetch-ensindexer-config.ts @@ -0,0 +1,17 @@ +import { + deserializeENSIndexerPublicConfig, + deserializeErrorResponse, + type SerializedENSIndexerPublicConfig, +} from "@ensnode/ensnode-sdk"; + +export async function fetchENSIndexerConfig(url: URL) { + const response = await fetch(new URL(`/api/config`, url)); + const responseData = await response.json(); + + if (!response.ok) { + const errorResponse = deserializeErrorResponse(responseData); + throw new Error(`Fetching ENSNode Config Failed: ${errorResponse.message}`); + } + + return deserializeENSIndexerPublicConfig(responseData as SerializedENSIndexerPublicConfig); +} diff --git a/packages/ensnode-react/src/hooks/index.ts b/packages/ensnode-react/src/hooks/index.ts index 49b99706c6..079265c60e 100644 --- a/packages/ensnode-react/src/hooks/index.ts +++ b/packages/ensnode-react/src/hooks/index.ts @@ -1,5 +1,5 @@ -export * from "./useENSIndexerConfig"; export * from "./useENSNodeConfig"; +export * from "./useENSNodeConfigQuery"; export * from "./useIndexingStatus"; export * from "./usePrimaryName"; export * from "./usePrimaryNames"; diff --git a/packages/ensnode-react/src/hooks/useENSIndexerConfig.ts b/packages/ensnode-react/src/hooks/useENSNodeConfigQuery.ts similarity index 60% rename from packages/ensnode-react/src/hooks/useENSIndexerConfig.ts rename to packages/ensnode-react/src/hooks/useENSNodeConfigQuery.ts index b44782447e..d4ff09d3d9 100644 --- a/packages/ensnode-react/src/hooks/useENSIndexerConfig.ts +++ b/packages/ensnode-react/src/hooks/useENSNodeConfigQuery.ts @@ -3,18 +3,18 @@ import { useQuery } from "@tanstack/react-query"; import type { ConfigResponse } from "@ensnode/ensnode-sdk"; import type { ConfigParameter, QueryParameter } from "../types"; -import { ASSUME_IMMUTABLE_QUERY, createENSIndexerConfigQueryOptions } from "../utils/query"; +import { ASSUME_IMMUTABLE_QUERY, createConfigQueryOptions } from "../utils/query"; import { useENSNodeConfig } from "./useENSNodeConfig"; -type UseENSIndexerConfigParameters = QueryParameter; +type UseENSNodeConfigParameters = QueryParameter; -export function useENSIndexerConfig( - parameters: ConfigParameter & UseENSIndexerConfigParameters = {}, +export function useENSNodeConfigQuery( + parameters: ConfigParameter & UseENSNodeConfigParameters = {}, ) { const { config, query = {} } = parameters; const _config = useENSNodeConfig(config); - const queryOptions = createENSIndexerConfigQueryOptions(_config); + const queryOptions = createConfigQueryOptions(_config); const options = { ...queryOptions, diff --git a/packages/ensnode-react/src/utils/query.ts b/packages/ensnode-react/src/utils/query.ts index 0762e467eb..50d4702417 100644 --- a/packages/ensnode-react/src/utils/query.ts +++ b/packages/ensnode-react/src/utils/query.ts @@ -109,9 +109,9 @@ export function createPrimaryNamesQueryOptions( } /** - * Creates query options for ENSIndexer Config API + * Creates query options for ENSNode Config API */ -export function createENSIndexerConfigQueryOptions(config: ENSNodeConfig) { +export function createConfigQueryOptions(config: ENSNodeConfig) { return { enabled: true, queryKey: queryKeys.config(config.client.url.href), @@ -123,7 +123,7 @@ export function createENSIndexerConfigQueryOptions(config: ENSNodeConfig) { } /** - * Creates query options for ENSIndexer Indexing Status API + * Creates query options for ENSNode Indexing Status API */ export function createIndexingStatusQueryOptions(config: ENSNodeConfig) { return { diff --git a/packages/ensnode-sdk/src/api/types.ts b/packages/ensnode-sdk/src/api/types.ts index 8739646d36..689427e5ce 100644 --- a/packages/ensnode-sdk/src/api/types.ts +++ b/packages/ensnode-sdk/src/api/types.ts @@ -1,6 +1,7 @@ import type z from "zod/v4"; -import type { ENSIndexerPublicConfig, RealtimeIndexingStatusProjection } from "../ensindexer"; +import type { ENSApiPublicConfig } from "../ensapi"; +import type { RealtimeIndexingStatusProjection } from "../ensindexer"; import type { ForwardResolutionArgs, MultichainPrimaryNameResolutionArgs, @@ -79,7 +80,7 @@ export interface ResolvePrimaryNamesResponse extends AcceleratableResponse, Trac /** * ENSIndexer Public Config Response */ -export type ConfigResponse = ENSIndexerPublicConfig; +export type ConfigResponse = ENSApiPublicConfig; /** * Represents a request to Indexing Status API. diff --git a/packages/ensnode-sdk/src/client.test.ts b/packages/ensnode-sdk/src/client.test.ts index 5f51983bf7..1840f98160 100644 --- a/packages/ensnode-sdk/src/client.test.ts +++ b/packages/ensnode-sdk/src/client.test.ts @@ -14,6 +14,7 @@ import { import { DEFAULT_ENSNODE_API_URL, ENSNodeClient } from "./client"; import { ClientError } from "./client-error"; import type { Name } from "./ens"; +import { deserializeENSApiPublicConfig, type SerializedENSApiPublicConfig } from "./ensapi"; import { ChainIndexingConfigTypeIds, ChainIndexingStatusIds, @@ -57,32 +58,35 @@ const EXAMPLE_PRIMARY_NAMES_RESPONSE = { const EXAMPLE_ERROR_RESPONSE: ErrorResponse = { message: "error" }; const EXAMPLE_CONFIG_RESPONSE = { - labelSet: { - labelSetId: "subgraph", - labelSetVersion: 0, - }, - indexedChainIds: [1, 8453, 59144, 10, 42161, 534352], - databaseSchemaName: "alphaSchema0.31.0", - isSubgraphCompatible: false, - namespace: "mainnet", - plugins: [ - PluginName.Subgraph, - PluginName.Basenames, - PluginName.Lineanames, - PluginName.ThreeDNS, - PluginName.ProtocolAcceleration, - PluginName.Referrals, - ], - versionInfo: { - nodejs: "22.18.0", - ponder: "0.11.43", - ensDb: "0.32.0", - ensIndexer: "0.32.0", - ensNormalize: "1.11.1", - ensRainbow: "0.31.0", - ensRainbowSchema: 2, + version: "0.32.0", + ensIndexerPublicConfig: { + labelSet: { + labelSetId: "subgraph", + labelSetVersion: 0, + }, + indexedChainIds: [1, 8453, 59144, 10, 42161, 534352], + databaseSchemaName: "alphaSchema0.31.0", + isSubgraphCompatible: false, + namespace: "mainnet", + plugins: [ + PluginName.Subgraph, + PluginName.Basenames, + PluginName.Lineanames, + PluginName.ThreeDNS, + PluginName.ProtocolAcceleration, + PluginName.Referrals, + ], + versionInfo: { + nodejs: "22.18.0", + ponder: "0.11.43", + ensDb: "0.32.0", + ensIndexer: "0.32.0", + ensNormalize: "1.11.1", + ensRainbow: "0.31.0", + ensRainbowSchema: 2, + }, }, -} satisfies SerializedENSIndexerPublicConfig; +} satisfies SerializedENSApiPublicConfig; const EXAMPLE_INDEXING_STATUS_BACKFILL_RESPONSE = deserializeIndexingStatusResponse({ realtimeProjection: { @@ -415,7 +419,7 @@ describe("ENSNodeClient", () => { // arrange const requestUrl = new URL(`/api/config`, DEFAULT_ENSNODE_API_URL); const serializedMockedResponse = EXAMPLE_CONFIG_RESPONSE; - const mockedResponse = deserializeENSIndexerPublicConfig(serializedMockedResponse); + const mockedResponse = deserializeENSApiPublicConfig(serializedMockedResponse); const client = new ENSNodeClient(); mockFetch.mockResolvedValueOnce({ diff --git a/packages/ensnode-sdk/src/client.ts b/packages/ensnode-sdk/src/client.ts index 2286cd2e5c..ac9b6d4037 100644 --- a/packages/ensnode-sdk/src/client.ts +++ b/packages/ensnode-sdk/src/client.ts @@ -15,10 +15,7 @@ import type { ResolveRecordsResponse, } from "./api/types"; import { ClientError } from "./client-error"; -import { - deserializeENSIndexerPublicConfig, - type SerializedENSIndexerPublicConfig, -} from "./ensindexer"; +import { deserializeENSApiPublicConfig, type SerializedENSApiPublicConfig } from "./ensapi"; import type { ResolverRecordsSelection } from "./resolution"; /** @@ -289,10 +286,9 @@ export class ENSNodeClient { const response = await fetch(url); - let responseData: unknown; - // ENSNode API should always allow parsing a response as JSON object. // If for some reason it's not the case, throw an error. + let responseData: unknown; try { responseData = await response.json(); } catch { @@ -304,7 +300,7 @@ export class ENSNodeClient { throw new Error(`Fetching ENSNode Config Failed: ${errorResponse.message}`); } - return deserializeENSIndexerPublicConfig(responseData as SerializedENSIndexerPublicConfig); + return deserializeENSApiPublicConfig(responseData as SerializedENSApiPublicConfig); } /** @@ -321,10 +317,9 @@ export class ENSNodeClient { const response = await fetch(url); - let responseData: unknown; - // ENSNode API should always allow parsing a response as JSON object. // If for some reason it's not the case, throw an error. + let responseData: unknown; try { responseData = await response.json(); } catch { @@ -333,9 +328,8 @@ export class ENSNodeClient { // handle response errors accordingly if (!response.ok) { - let errorResponse: ErrorResponse | undefined; - // check for a generic errorResponse + let errorResponse: ErrorResponse | undefined; try { errorResponse = deserializeErrorResponse(responseData); } catch { diff --git a/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts b/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts new file mode 100644 index 0000000000..3fdc2970a2 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; + +import { ENSNamespaceIds } from "@ensnode/datasources"; + +import { PluginName } from "../../ensindexer"; +import { deserializeENSApiPublicConfig, serializeENSApiPublicConfig } from "."; +import type { ENSApiPublicConfig } from "./types"; + +const MOCK_ENSAPI_PUBLIC_CONFIG = { + version: "0.36.0", + ensIndexerPublicConfig: { + namespace: ENSNamespaceIds.Mainnet, + databaseSchemaName: "ensapi", + indexedChainIds: new Set([1]), + isSubgraphCompatible: false, + labelSet: { labelSetId: "subgraph", labelSetVersion: 0 }, + plugins: [PluginName.Subgraph], + versionInfo: { + ensDb: "0.36.0", + ensIndexer: "0.36.0", + ensRainbow: "0.36.0", + ensRainbowSchema: 1, + ensNormalize: "1.1.1", + nodejs: "20.0.0", + ponder: "0.5.0", + }, + }, +} satisfies ENSApiPublicConfig; + +const MOCK_SERIALIZED_ENSAPI_PUBLIC_CONFIG = serializeENSApiPublicConfig(MOCK_ENSAPI_PUBLIC_CONFIG); + +describe("ENSApi Config Serialization/Deserialization", () => { + describe("serializeENSApiPublicConfig", () => { + it("serializes ENSAPI public config correctly", () => { + const result = serializeENSApiPublicConfig(MOCK_ENSAPI_PUBLIC_CONFIG); + + expect(result).toEqual({ + version: "0.36.0", + ensIndexerPublicConfig: { + namespace: ENSNamespaceIds.Mainnet, + databaseSchemaName: "ensapi", + indexedChainIds: [1], + isSubgraphCompatible: false, + labelSet: { labelSetId: "subgraph", labelSetVersion: 0 }, + plugins: [PluginName.Subgraph], + versionInfo: { + ensDb: "0.36.0", + ensIndexer: "0.36.0", + ensRainbow: "0.36.0", + ensRainbowSchema: 1, + ensNormalize: "1.1.1", + nodejs: "20.0.0", + ponder: "0.5.0", + }, + }, + }); + }); + }); + + describe("deserializeENSApiPublicConfig", () => { + it("deserializes ENSAPI public config correctly", () => { + const serialized = serializeENSApiPublicConfig(MOCK_ENSAPI_PUBLIC_CONFIG); + const result = deserializeENSApiPublicConfig(serialized); + + expect(result).toEqual(MOCK_ENSAPI_PUBLIC_CONFIG); + }); + + it("handles validation errors with custom value label", () => { + const invalidConfig = { + ...MOCK_SERIALIZED_ENSAPI_PUBLIC_CONFIG, + version: "", // Invalid: empty string + }; + + expect(() => deserializeENSApiPublicConfig(invalidConfig, "testConfig")).toThrow( + /testConfig.version/, + ); + }); + }); + + describe("round-trip conversion", () => { + it("maintains data integrity through serialize -> deserialize cycle", () => { + const serialized = serializeENSApiPublicConfig(MOCK_ENSAPI_PUBLIC_CONFIG); + const deserialized = deserializeENSApiPublicConfig(serialized); + + expect(deserialized).toStrictEqual(MOCK_ENSAPI_PUBLIC_CONFIG); + }); + }); +}); diff --git a/packages/ensnode-sdk/src/ensapi/config/deserialize.ts b/packages/ensnode-sdk/src/ensapi/config/deserialize.ts new file mode 100644 index 0000000000..0daa541bd2 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/deserialize.ts @@ -0,0 +1,24 @@ +import { prettifyError, ZodError } from "zod/v4"; + +import type { SerializedENSApiPublicConfig } from "./serialized-types"; +import type { ENSApiPublicConfig } from "./types"; +import { makeENSApiPublicConfigSchema } from "./zod-schemas"; + +/** + * Deserialize a {@link ENSApiPublicConfig} object. + */ +export function deserializeENSApiPublicConfig( + maybeConfig: SerializedENSApiPublicConfig, + valueLabel?: string, +): ENSApiPublicConfig { + const schema = makeENSApiPublicConfigSchema(valueLabel); + try { + return schema.parse(maybeConfig); + } catch (error) { + if (error instanceof ZodError) { + throw new Error(`Cannot deserialize ENSApiPublicConfig:\n${prettifyError(error)}\n`); + } + + throw error; + } +} diff --git a/packages/ensnode-sdk/src/ensapi/config/index.ts b/packages/ensnode-sdk/src/ensapi/config/index.ts new file mode 100644 index 0000000000..bff2897b57 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/index.ts @@ -0,0 +1,5 @@ +export * from "./deserialize"; +export * from "./serialize"; +export * from "./serialized-types"; +export * from "./types"; +export * from "./zod-schemas"; diff --git a/packages/ensnode-sdk/src/ensapi/config/serialize.ts b/packages/ensnode-sdk/src/ensapi/config/serialize.ts new file mode 100644 index 0000000000..4741e376b5 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/serialize.ts @@ -0,0 +1,17 @@ +import { serializeENSIndexerPublicConfig } from "../../ensindexer"; +import type { SerializedENSApiPublicConfig } from "./serialized-types"; +import type { ENSApiPublicConfig } from "./types"; + +/** + * Serialize a {@link ENSApiPublicConfig} object. + */ +export function serializeENSApiPublicConfig( + config: ENSApiPublicConfig, +): SerializedENSApiPublicConfig { + const { version, ensIndexerPublicConfig } = config; + + return { + version, + ensIndexerPublicConfig: serializeENSIndexerPublicConfig(ensIndexerPublicConfig), + } satisfies SerializedENSApiPublicConfig; +} diff --git a/packages/ensnode-sdk/src/ensapi/config/serialized-types.ts b/packages/ensnode-sdk/src/ensapi/config/serialized-types.ts new file mode 100644 index 0000000000..f437da884a --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/serialized-types.ts @@ -0,0 +1,13 @@ +import type { SerializedENSIndexerPublicConfig } from "../../ensindexer"; +import type { ENSApiPublicConfig } from "./types"; + +/** + * Serialized representation of {@link ENSApiPublicConfig} + */ +export interface SerializedENSApiPublicConfig + extends Omit { + /** + * Serialized representation of {@link ENSApiPublicConfig.ensIndexerPublicConfig}. + */ + ensIndexerPublicConfig: SerializedENSIndexerPublicConfig; +} diff --git a/packages/ensnode-sdk/src/ensapi/config/types.ts b/packages/ensnode-sdk/src/ensapi/config/types.ts new file mode 100644 index 0000000000..5af6bc59ab --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/types.ts @@ -0,0 +1,24 @@ +import type { ENSIndexerPublicConfig } from "../../ensindexer"; + +/** + * Complete public configuration object for ENSApi. + * + * Contains ENSApi-specific configuration at the top level and + * embeds the complete ENSIndexer public configuration. + */ +export interface ENSApiPublicConfig { + /** + * ENSApi service version + * + * @see https://ghcr.io/namehash/ensnode/ensapi + */ + version: string; + + /** + * Complete ENSIndexer public configuration + * + * Contains all ENSIndexer public configuration including + * namespace, plugins, version info, etc. + */ + ensIndexerPublicConfig: ENSIndexerPublicConfig; +} diff --git a/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts b/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts new file mode 100644 index 0000000000..f495c995d9 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts @@ -0,0 +1,17 @@ +import { z } from "zod/v4"; + +import { makeENSIndexerPublicConfigSchema } from "../../ensindexer/config/zod-schemas"; + +/** + * Create a Zod schema for validating a serialized ENSApiPublicConfig. + * + * @param valueLabel - Optional label for the value being validated (used in error messages) + */ +export function makeENSApiPublicConfigSchema(valueLabel?: string) { + const label = valueLabel ?? "ENSApiPublicConfig"; + + return z.strictObject({ + version: z.string().min(1, `${label}.version must be a non-empty string`), + ensIndexerPublicConfig: makeENSIndexerPublicConfigSchema(`${label}.ensIndexerPublicConfig`), + }); +} diff --git a/packages/ensnode-sdk/src/ensapi/index.ts b/packages/ensnode-sdk/src/ensapi/index.ts new file mode 100644 index 0000000000..5c62e04f5e --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/index.ts @@ -0,0 +1 @@ +export * from "./config"; diff --git a/packages/ensnode-sdk/src/index.ts b/packages/ensnode-sdk/src/index.ts index 32dfbeec60..32332cf0c7 100644 --- a/packages/ensnode-sdk/src/index.ts +++ b/packages/ensnode-sdk/src/index.ts @@ -6,6 +6,7 @@ export { } from "./client"; export * from "./client-error"; export * from "./ens"; +export * from "./ensapi"; export * from "./ensindexer"; export * from "./ensrainbow"; export * from "./resolution"; From a757475fe561f99393d5cdb769330a4475c74da4 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 16:48:09 -0500 Subject: [PATCH 04/15] docs(changeset): BREAKING: client.config() now returns Promise instead of ENSIndexerPublicConfig. --- .changeset/shaky-schools-nail.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shaky-schools-nail.md diff --git a/.changeset/shaky-schools-nail.md b/.changeset/shaky-schools-nail.md new file mode 100644 index 0000000000..ffd51a53b6 --- /dev/null +++ b/.changeset/shaky-schools-nail.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-sdk": minor +--- + +BREAKING: client.config() now returns Promise instead of ENSIndexerPublicConfig. From 339bb9e5aea00a27a649654c566e3f6fb6427bdd Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 16:49:06 -0500 Subject: [PATCH 05/15] docs(changeset): BREAKING: `useENSNodeConfig` has been renamed to `useENSNodeSDKConfig`. `useENSIndexerConfig` has been renamed to `useENSNodeConfig`. --- .changeset/young-badgers-trade.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/young-badgers-trade.md diff --git a/.changeset/young-badgers-trade.md b/.changeset/young-badgers-trade.md new file mode 100644 index 0000000000..812a843358 --- /dev/null +++ b/.changeset/young-badgers-trade.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-react": minor +--- + +BREAKING: `useENSNodeConfig` has been renamed to `useENSNodeSDKConfig`. `useENSIndexerConfig` has been renamed to `useENSNodeConfig`. From 811483712aa2310fe30cf3a32c85365fa55d9642 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 16:49:33 -0500 Subject: [PATCH 06/15] docs(changeset): ENSAdmin now supports ENSApi Version info. --- .changeset/nine-ducks-lick.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nine-ducks-lick.md diff --git a/.changeset/nine-ducks-lick.md b/.changeset/nine-ducks-lick.md new file mode 100644 index 0000000000..c86240587c --- /dev/null +++ b/.changeset/nine-ducks-lick.md @@ -0,0 +1,5 @@ +--- +"ensadmin": minor +--- + +ENSAdmin now supports ENSApi Version info. From eaacbc3513ff29c20661e8c5464f45e8f9a3cf14 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 16:50:28 -0500 Subject: [PATCH 07/15] feat: rename hooks for clarity --- .../src/components/connection/index.tsx | 4 +- .../connections/require-active-connection.tsx | 4 +- .../hooks/active/use-active-connection.tsx | 4 +- .../ensadmin/src/hooks/async/use-namespace.ts | 4 +- packages/ensnode-react/src/context.ts | 4 +- packages/ensnode-react/src/hooks/index.ts | 2 +- .../src/hooks/useENSNodeConfig.ts | 43 +++++++++---------- .../src/hooks/useENSNodeConfigQuery.ts | 27 ------------ .../src/hooks/useENSNodeSDKConfig.ts | 30 +++++++++++++ .../src/hooks/useIndexingStatus.ts | 10 +++-- .../ensnode-react/src/hooks/usePrimaryName.ts | 8 ++-- .../src/hooks/usePrimaryNames.ts | 8 ++-- .../ensnode-react/src/hooks/useRecords.ts | 8 ++-- packages/ensnode-react/src/provider.tsx | 8 ++-- packages/ensnode-react/src/types.ts | 4 +- packages/ensnode-react/src/utils/query.ts | 12 +++--- 16 files changed, 91 insertions(+), 89 deletions(-) delete mode 100644 packages/ensnode-react/src/hooks/useENSNodeConfigQuery.ts create mode 100644 packages/ensnode-react/src/hooks/useENSNodeSDKConfig.ts diff --git a/apps/ensadmin/src/components/connection/index.tsx b/apps/ensadmin/src/components/connection/index.tsx index a92150cdd1..3860f55711 100644 --- a/apps/ensadmin/src/components/connection/index.tsx +++ b/apps/ensadmin/src/components/connection/index.tsx @@ -2,7 +2,7 @@ import { use } from "react"; -import { useENSNodeConfigQuery } from "@ensnode/ensnode-react"; +import { useENSNodeConfig } from "@ensnode/ensnode-react"; import { ENSNodeConfigInfo } from "@/components/connection/config-info"; import { ensAdminVersion } from "@/lib/env"; @@ -11,7 +11,7 @@ const versionPromise = ensAdminVersion(); export default function ConnectionInfo() { const version = use(versionPromise); - const { status, error, data } = useENSNodeConfigQuery(); + const { status, error, data } = useENSNodeConfig(); if (status === "pending") { return ( diff --git a/apps/ensadmin/src/components/connections/require-active-connection.tsx b/apps/ensadmin/src/components/connections/require-active-connection.tsx index c95f235cde..683f4308a7 100644 --- a/apps/ensadmin/src/components/connections/require-active-connection.tsx +++ b/apps/ensadmin/src/components/connections/require-active-connection.tsx @@ -2,7 +2,7 @@ import type { PropsWithChildren } from "react"; -import { useENSNodeConfigQuery } from "@ensnode/ensnode-react"; +import { useENSNodeConfig } from "@ensnode/ensnode-react"; import { ErrorInfo } from "@/components/error-info"; import { LoadingSpinner } from "@/components/loading-spinner"; @@ -11,7 +11,7 @@ import { LoadingSpinner } from "@/components/loading-spinner"; * Allows consumers to use `useActiveConnection` by blocking rendering until it is available. */ export function RequireActiveConnection({ children }: PropsWithChildren) { - const { status, error } = useENSNodeConfigQuery(); + const { status, error } = useENSNodeConfig(); if (status === "pending") return ; diff --git a/apps/ensadmin/src/hooks/active/use-active-connection.tsx b/apps/ensadmin/src/hooks/active/use-active-connection.tsx index fd40f00f08..e1fa783caf 100644 --- a/apps/ensadmin/src/hooks/active/use-active-connection.tsx +++ b/apps/ensadmin/src/hooks/active/use-active-connection.tsx @@ -1,6 +1,6 @@ "use client"; -import { useENSNodeConfigQuery } from "@ensnode/ensnode-react"; +import { useENSNodeConfig } from "@ensnode/ensnode-react"; /** * Hook to get the currently active ENSNode connection synchronously. @@ -16,7 +16,7 @@ import { useENSNodeConfigQuery } from "@ensnode/ensnode-react"; * @throws Error if no active ENSNode connection is available */ export function useActiveConnection() { - const { data } = useENSNodeConfigQuery(); + const { data } = useENSNodeConfig(); if (data === undefined) { throw new Error(`Invariant(useActiveConnection): Expected an active ENSNode Config`); diff --git a/apps/ensadmin/src/hooks/async/use-namespace.ts b/apps/ensadmin/src/hooks/async/use-namespace.ts index 0e04e1cdc7..be3a0925ed 100644 --- a/apps/ensadmin/src/hooks/async/use-namespace.ts +++ b/apps/ensadmin/src/hooks/async/use-namespace.ts @@ -1,4 +1,4 @@ -import { useENSNodeConfigQuery } from "@ensnode/ensnode-react"; +import { useENSNodeConfig } from "@ensnode/ensnode-react"; /** * Hook to get the namespace ID from the active ENSNode connection. @@ -22,7 +22,7 @@ import { useENSNodeConfigQuery } from "@ensnode/ensnode-react"; * ``` */ export function useNamespace() { - const query = useENSNodeConfigQuery(); + const query = useENSNodeConfig(); return { ...query, diff --git a/packages/ensnode-react/src/context.ts b/packages/ensnode-react/src/context.ts index db97bad131..42474d2430 100644 --- a/packages/ensnode-react/src/context.ts +++ b/packages/ensnode-react/src/context.ts @@ -1,11 +1,11 @@ import { createContext } from "react"; -import type { ENSNodeConfig } from "./types"; +import type { ENSNodeSDKConfig } from "./types"; /** * React context for ENSNode configuration */ -export const ENSNodeContext = createContext(undefined); +export const ENSNodeContext = createContext(undefined); /** * Display name for debugging diff --git a/packages/ensnode-react/src/hooks/index.ts b/packages/ensnode-react/src/hooks/index.ts index 079265c60e..0d59970704 100644 --- a/packages/ensnode-react/src/hooks/index.ts +++ b/packages/ensnode-react/src/hooks/index.ts @@ -1,5 +1,5 @@ export * from "./useENSNodeConfig"; -export * from "./useENSNodeConfigQuery"; +export * from "./useENSNodeSDKConfig"; export * from "./useIndexingStatus"; export * from "./usePrimaryName"; export * from "./usePrimaryNames"; diff --git a/packages/ensnode-react/src/hooks/useENSNodeConfig.ts b/packages/ensnode-react/src/hooks/useENSNodeConfig.ts index e1d3c297fa..c246d61afd 100644 --- a/packages/ensnode-react/src/hooks/useENSNodeConfig.ts +++ b/packages/ensnode-react/src/hooks/useENSNodeConfig.ts @@ -1,30 +1,27 @@ -"use client"; +import { useQuery } from "@tanstack/react-query"; -import { useContext } from "react"; +import type { ConfigResponse } from "@ensnode/ensnode-sdk"; -import { ENSNodeContext } from "../context"; -import type { ENSNodeConfig } from "../types"; +import type { QueryParameter, WithSDKConfigParameter } from "../types"; +import { ASSUME_IMMUTABLE_QUERY, createConfigQueryOptions } from "../utils/query"; +import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; -/** - * Hook to access the ENSNode configuration from context or parameters - * - * @param parameters - Optional config parameter that overrides context - * @returns The ENSNode configuration - * @throws Error if no config is available in context or parameters - */ -export function useENSNodeConfig( - config: TConfig | undefined, -): TConfig { - const contextConfig = useContext(ENSNodeContext); +type UseENSNodeConfigParameters = QueryParameter; - // Use provided config or fall back to context - const resolvedConfig = config ?? contextConfig; +export function useENSNodeConfig( + parameters: WithSDKConfigParameter & UseENSNodeConfigParameters = {}, +) { + const { config, query = {} } = parameters; + const _config = useENSNodeSDKConfig(config); - if (!resolvedConfig) { - throw new Error( - "useENSNodeConfig must be used within an ENSNodeProvider or you must pass a config parameter", - ); - } + const queryOptions = createConfigQueryOptions(_config); - return resolvedConfig as TConfig; + const options = { + ...queryOptions, + ...ASSUME_IMMUTABLE_QUERY, + ...query, + enabled: query.enabled ?? queryOptions.enabled, + }; + + return useQuery(options); } diff --git a/packages/ensnode-react/src/hooks/useENSNodeConfigQuery.ts b/packages/ensnode-react/src/hooks/useENSNodeConfigQuery.ts deleted file mode 100644 index d4ff09d3d9..0000000000 --- a/packages/ensnode-react/src/hooks/useENSNodeConfigQuery.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import type { ConfigResponse } from "@ensnode/ensnode-sdk"; - -import type { ConfigParameter, QueryParameter } from "../types"; -import { ASSUME_IMMUTABLE_QUERY, createConfigQueryOptions } from "../utils/query"; -import { useENSNodeConfig } from "./useENSNodeConfig"; - -type UseENSNodeConfigParameters = QueryParameter; - -export function useENSNodeConfigQuery( - parameters: ConfigParameter & UseENSNodeConfigParameters = {}, -) { - const { config, query = {} } = parameters; - const _config = useENSNodeConfig(config); - - const queryOptions = createConfigQueryOptions(_config); - - const options = { - ...queryOptions, - ...ASSUME_IMMUTABLE_QUERY, - ...query, - enabled: query.enabled ?? queryOptions.enabled, - }; - - return useQuery(options); -} diff --git a/packages/ensnode-react/src/hooks/useENSNodeSDKConfig.ts b/packages/ensnode-react/src/hooks/useENSNodeSDKConfig.ts new file mode 100644 index 0000000000..2f7b1016aa --- /dev/null +++ b/packages/ensnode-react/src/hooks/useENSNodeSDKConfig.ts @@ -0,0 +1,30 @@ +"use client"; + +import { useContext } from "react"; + +import { ENSNodeContext } from "../context"; +import type { ENSNodeSDKConfig } from "../types"; + +/** + * Hook to access the ENSNodeSDKConfig from context or parameters. + * + * @param parameters - Optional config parameter that overrides context + * @returns The ENSNode configuration + * @throws Error if no config is available in context or parameters + */ +export function useENSNodeSDKConfig( + config: TConfig | undefined, +): TConfig { + const contextConfig = useContext(ENSNodeContext); + + // Use provided config or fall back to context + const resolvedConfig = config ?? contextConfig; + + if (!resolvedConfig) { + throw new Error( + "useENSNodeSDKConfig must be used within an ENSNodeProvider or you must pass a config parameter", + ); + } + + return resolvedConfig as TConfig; +} diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index d587a434ad..c56f3d6928 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -2,17 +2,19 @@ import { useQuery } from "@tanstack/react-query"; import type { IndexingStatusRequest, IndexingStatusResponse } from "@ensnode/ensnode-sdk"; -import type { ConfigParameter, QueryParameter } from "../types"; +import type { QueryParameter, WithSDKConfigParameter } from "../types"; import { createIndexingStatusQueryOptions } from "../utils/query"; -import { useENSNodeConfig } from "./useENSNodeConfig"; +import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; interface UseIndexingStatusParameters extends IndexingStatusRequest, QueryParameter {} -export function useIndexingStatus(parameters: ConfigParameter & UseIndexingStatusParameters = {}) { +export function useIndexingStatus( + parameters: WithSDKConfigParameter & UseIndexingStatusParameters = {}, +) { const { config, query = {} } = parameters; - const _config = useENSNodeConfig(config); + const _config = useENSNodeSDKConfig(config); const queryOptions = createIndexingStatusQueryOptions(_config); diff --git a/packages/ensnode-react/src/hooks/usePrimaryName.ts b/packages/ensnode-react/src/hooks/usePrimaryName.ts index b823c357fd..2bd7c9ffba 100644 --- a/packages/ensnode-react/src/hooks/usePrimaryName.ts +++ b/packages/ensnode-react/src/hooks/usePrimaryName.ts @@ -2,9 +2,9 @@ import { useQuery } from "@tanstack/react-query"; -import type { ConfigParameter, UsePrimaryNameParameters } from "../types"; +import type { UsePrimaryNameParameters, WithSDKConfigParameter } from "../types"; import { createPrimaryNameQueryOptions } from "../utils/query"; -import { useENSNodeConfig } from "./useENSNodeConfig"; +import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; /** * Resolves the primary name of a specified address (Reverse Resolution). @@ -37,9 +37,9 @@ import { useENSNodeConfig } from "./useENSNodeConfig"; * } * ``` */ -export function usePrimaryName(parameters: UsePrimaryNameParameters & ConfigParameter) { +export function usePrimaryName(parameters: UsePrimaryNameParameters & WithSDKConfigParameter) { const { config, query = {}, address, ...args } = parameters; - const _config = useENSNodeConfig(config); + const _config = useENSNodeSDKConfig(config); const canEnable = address !== null; diff --git a/packages/ensnode-react/src/hooks/usePrimaryNames.ts b/packages/ensnode-react/src/hooks/usePrimaryNames.ts index 0d02d53c5f..4c3f9026e3 100644 --- a/packages/ensnode-react/src/hooks/usePrimaryNames.ts +++ b/packages/ensnode-react/src/hooks/usePrimaryNames.ts @@ -2,9 +2,9 @@ import { useQuery } from "@tanstack/react-query"; -import type { ConfigParameter, UsePrimaryNamesParameters } from "../types"; +import type { UsePrimaryNamesParameters, WithSDKConfigParameter } from "../types"; import { createPrimaryNamesQueryOptions } from "../utils/query"; -import { useENSNodeConfig } from "./useENSNodeConfig"; +import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; /** * Resolves the primary names of a specified address across multiple chains. @@ -40,9 +40,9 @@ import { useENSNodeConfig } from "./useENSNodeConfig"; * } * ``` */ -export function usePrimaryNames(parameters: UsePrimaryNamesParameters & ConfigParameter) { +export function usePrimaryNames(parameters: UsePrimaryNamesParameters & WithSDKConfigParameter) { const { config, query = {}, address, ...args } = parameters; - const _config = useENSNodeConfig(config); + const _config = useENSNodeSDKConfig(config); const canEnable = address !== null; diff --git a/packages/ensnode-react/src/hooks/useRecords.ts b/packages/ensnode-react/src/hooks/useRecords.ts index e3ef4c0ea8..ee7d9ed9f9 100644 --- a/packages/ensnode-react/src/hooks/useRecords.ts +++ b/packages/ensnode-react/src/hooks/useRecords.ts @@ -4,9 +4,9 @@ import { useQuery } from "@tanstack/react-query"; import type { ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; -import type { ConfigParameter, UseRecordsParameters } from "../types"; +import type { UseRecordsParameters, WithSDKConfigParameter } from "../types"; import { createRecordsQueryOptions } from "../utils/query"; -import { useENSNodeConfig } from "./useENSNodeConfig"; +import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; /** * Resolves records for an ENS name (Forward Resolution). @@ -51,10 +51,10 @@ import { useENSNodeConfig } from "./useENSNodeConfig"; * ``` */ export function useRecords( - parameters: UseRecordsParameters & ConfigParameter, + parameters: UseRecordsParameters & WithSDKConfigParameter, ) { const { config, query = {}, name, ...args } = parameters; - const _config = useENSNodeConfig(config); + const _config = useENSNodeSDKConfig(config); const canEnable = name !== null; diff --git a/packages/ensnode-react/src/provider.tsx b/packages/ensnode-react/src/provider.tsx index 83975653b6..14b8df451b 100644 --- a/packages/ensnode-react/src/provider.tsx +++ b/packages/ensnode-react/src/provider.tsx @@ -7,11 +7,11 @@ import { createElement, useMemo } from "react"; import { ENSNodeClient } from "@ensnode/ensnode-sdk"; import { ENSNodeContext } from "./context"; -import type { ENSNodeConfig } from "./types"; +import type { ENSNodeSDKConfig } from "./types"; export interface ENSNodeProviderProps { /** ENSNode configuration */ - config: ENSNodeConfig; + config: ENSNodeSDKConfig; /** * Optional QueryClient instance. If provided, you must wrap your app with QueryClientProvider yourself. @@ -31,7 +31,7 @@ function ENSNodeInternalProvider({ config, }: { children: React.ReactNode; - config: ENSNodeConfig; + config: ENSNodeSDKConfig; }) { // Memoize the config to prevent unnecessary re-renders const memoizedConfig = useMemo(() => config, [config]); @@ -94,7 +94,7 @@ export function ENSNodeProvider(parameters: React.PropsWithChildren { /** * Configuration parameter for hooks that need access to config */ -export interface ConfigParameter { +export interface WithSDKConfigParameter { config?: TConfig | undefined; } diff --git a/packages/ensnode-react/src/utils/query.ts b/packages/ensnode-react/src/utils/query.ts index 50d4702417..6e77b25df6 100644 --- a/packages/ensnode-react/src/utils/query.ts +++ b/packages/ensnode-react/src/utils/query.ts @@ -10,7 +10,7 @@ import { type ResolverRecordsSelection, } from "@ensnode/ensnode-sdk"; -import type { ENSNodeConfig } from "../types"; +import type { ENSNodeSDKConfig } from "../types"; /** * Immutable query options for data that is assumed to be immutable and should only be fetched once per full page refresh per unique key. @@ -61,7 +61,7 @@ export const queryKeys = { * Creates query options for Records Resolution */ export function createRecordsQueryOptions( - config: ENSNodeConfig, + config: ENSNodeSDKConfig, args: ResolveRecordsRequest, ) { return { @@ -78,7 +78,7 @@ export function createRecordsQueryOptions Date: Mon, 27 Oct 2025 17:16:35 -0500 Subject: [PATCH 08/15] docs(changeset): BREAKING: Removed DefaultRecordsSelection export: integrating apps should define their own set of records to request when using useRecords(). --- .changeset/brown-readers-talk.md | 5 +++++ .../ensadmin/src/lib}/default-records-selection.ts | 0 2 files changed, 5 insertions(+) create mode 100644 .changeset/brown-readers-talk.md rename {packages/ensnode-sdk/src/resolution => apps/ensadmin/src/lib}/default-records-selection.ts (100%) diff --git a/.changeset/brown-readers-talk.md b/.changeset/brown-readers-talk.md new file mode 100644 index 0000000000..de2efcee7b --- /dev/null +++ b/.changeset/brown-readers-talk.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-sdk": minor +--- + +BREAKING: Removed DefaultRecordsSelection export: integrating apps should define their own set of records to request when using useRecords(). diff --git a/packages/ensnode-sdk/src/resolution/default-records-selection.ts b/apps/ensadmin/src/lib/default-records-selection.ts similarity index 100% rename from packages/ensnode-sdk/src/resolution/default-records-selection.ts rename to apps/ensadmin/src/lib/default-records-selection.ts From 242f4ccc39eb91df77a96bc24d32a7641403ff1d Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 17:38:41 -0500 Subject: [PATCH 09/15] feat: integrate acceleration attempted logic for better tracing page --- .../_components/render-requests-output.tsx | 128 +++++++----------- .../ensadmin/src/app/inspect/records/page.tsx | 9 +- .../src/lib/default-records-selection.ts | 19 +-- packages/ensnode-sdk/src/resolution/index.ts | 1 - 4 files changed, 64 insertions(+), 93 deletions(-) diff --git a/apps/ensadmin/src/app/inspect/_components/render-requests-output.tsx b/apps/ensadmin/src/app/inspect/_components/render-requests-output.tsx index fcce26208e..2e3257124b 100644 --- a/apps/ensadmin/src/app/inspect/_components/render-requests-output.tsx +++ b/apps/ensadmin/src/app/inspect/_components/render-requests-output.tsx @@ -1,5 +1,5 @@ import type { UseQueryResult } from "@tanstack/react-query"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { type AcceleratableResponse, @@ -36,29 +36,23 @@ export function RenderRequestsOutput({ }) { const [tab, setTab] = useState("accelerated"); - // TODO: produce a diff between accelerated/not-accelerated and display any differences - const result = useMemo(() => { - if (tab === "accelerated" && accelerated.status === "success") { - return accelerated.data[dataKey]; - } + const focused = useMemo(() => { + if (tab === "accelerated") return accelerated; + if (tab === "unaccelerated") return unaccelerated; - if (tab === "unaccelerated" && unaccelerated.status === "success") { - return unaccelerated.data[dataKey]; - } - - return accelerated.data?.[dataKey] || unaccelerated.data?.[dataKey]; - }, [accelerated, unaccelerated, tab, dataKey]); + throw new Error("never"); + }, [accelerated, unaccelerated]); - const someError = accelerated.error || unaccelerated.error; + // need special derivation to capture refetching state + const acceleratedLoading = accelerated.isPending || accelerated.isRefetching; + const unacceleratedLoading = unaccelerated.isPending || unaccelerated.isRefetching; - // show major loading if either query is pending/refreshing - const showLoading = - (accelerated.isPending || accelerated.isRefetching) && - (unaccelerated.isPending || unaccelerated.isRefetching); + const acceleratedSuccess = !acceleratedLoading && !accelerated.isError; + const unacceleratedSuccess = !unacceleratedLoading && !unaccelerated.isError; const multipleDiff = useMemo(() => { - if (accelerated.status !== "success") return null; - if (unaccelerated.status !== "success") return null; + if (!acceleratedSuccess) return null; + if (!unacceleratedSuccess) return null; if (!accelerated.data.trace) return null; if (!unaccelerated.data.trace) return null; @@ -66,17 +60,21 @@ export function RenderRequestsOutput({ const acceleratedDuration = getTraceDuration(accelerated.data.trace); const unacceleratedDuration = getTraceDuration(unaccelerated.data.trace); - if (acceleratedDuration === 0) return null; + if (acceleratedDuration === 0) return null; // prevent division by zero... - const multiple = unacceleratedDuration / acceleratedDuration; - return multiple; + return unacceleratedDuration / acceleratedDuration; }, [accelerated, unaccelerated]); - if (showLoading) { - // if we're loading but there's no active fetch, the query is unable to be executed, so render null - if (accelerated.fetchStatus === "idle") return null; + useEffect(() => { + if (unacceleratedLoading) setTab("accelerated"); + }, [unacceleratedLoading, setTab]); + + // if we're loading but there's no active fetch, the query is unable to be executed, so render null + const isNotExecutable = acceleratedLoading && accelerated.fetchStatus === "idle"; + if (isNotExecutable) return null; - // otherwise, we're in-flight, render loading + // show major loading if accelerated query is pending/refreshing + if (acceleratedLoading) { return ( @@ -90,20 +88,21 @@ export function RenderRequestsOutput({ return ( <> + {/* Response Card */} ENSNode Response {(() => { - if (someError) { + if (focused.error) { return ( {JSON.stringify( { - message: someError.message, - ...(someError instanceof ClientError && - !!someError.details && { details: someError.details }), + message: focused.error.message, + ...(focused.error instanceof ClientError && + !!focused.error.details && { details: focused.error.details }), }, null, 2, @@ -114,13 +113,15 @@ export function RenderRequestsOutput({ return ( - {JSON.stringify(result, null, 2)} + {JSON.stringify(focused.data?.[dataKey], null, 2)} ); })()} - {!someError && (accelerated.data?.trace || unaccelerated.data?.trace) && ( + + {/* Execution Trace Card */} + {acceleratedSuccess && ( @@ -156,25 +157,24 @@ export function RenderRequestsOutput({ return null; })()} + Accelerated - {accelerated.data ? ( - `(${ - // biome-ignore lint/style/noNonNullAssertion: exists - renderTraceDuration(accelerated.data.trace!) - })` + {acceleratedSuccess && accelerated.data.trace ? ( + `(${renderTraceDuration(accelerated.data.trace)})` ) : ( )} - + Unaccelerated - {unaccelerated.data ? ( - `(${ - // biome-ignore lint/style/noNonNullAssertion: exists - renderTraceDuration(unaccelerated.data.trace!) - })` + {unacceleratedSuccess && unaccelerated.data.trace ? ( + `(${renderTraceDuration(unaccelerated.data.trace)})` ) : ( )} @@ -184,44 +184,14 @@ export function RenderRequestsOutput({ - {(() => { - switch (accelerated.status) { - case "pending": { - return ( -
- -
- ); - } - case "success": { - if (accelerated.data.trace) - return ; - throw new Error( - "Invariant: RenderRequestsOutput accelerated.data.trace is undefined.", - ); - } - } - })()} + {acceleratedSuccess && !!accelerated.data.trace && ( + + )}
- {(() => { - switch (unaccelerated.status) { - case "pending": { - return ( -
- -
- ); - } - case "success": { - if (unaccelerated.data.trace) - return ; - throw new Error( - "Invariant: RenderRequestsOutput unaccelerated.data.trace is undefined.", - ); - } - } - })()} + {unacceleratedSuccess && !!unaccelerated.data.trace && ( + + )}
diff --git a/apps/ensadmin/src/app/inspect/records/page.tsx b/apps/ensadmin/src/app/inspect/records/page.tsx index 893e93f4a8..caec8c8e33 100644 --- a/apps/ensadmin/src/app/inspect/records/page.tsx +++ b/apps/ensadmin/src/app/inspect/records/page.tsx @@ -5,7 +5,6 @@ import { useState } from "react"; import { useDebouncedValue } from "rooks"; import { useRecords } from "@ensnode/ensnode-react"; -import { DefaultRecordsSelection } from "@ensnode/ensnode-sdk"; import { RenderRequestsOutput } from "@/app/inspect/_components/render-requests-output"; import { Pill } from "@/components/pill"; @@ -13,6 +12,8 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { useActiveNamespace } from "@/hooks/active/use-active-namespace"; +import { DefaultRecordsSelection } from "@/lib/default-records-selection"; const EXAMPLE_INPUT = [ "vitalik.eth", @@ -30,8 +31,9 @@ const EXAMPLE_INPUT = [ // TODO: showcase current ENSNode configuration and viable acceleration pathways? // TODO: use shadcn/form, react-hook-form, and zod to make all of this nicer aross the board -// TODO: sync form state to query params, current just defaulting is supported +// TODO: sync form state to query params, currently just defaulting is supported export default function ResolveRecordsInspector() { + const namespace = useActiveNamespace(); const searchParams = useSearchParams(); const [name, setName] = useState(searchParams.get("name") || EXAMPLE_INPUT[0]); @@ -39,8 +41,7 @@ export default function ResolveRecordsInspector() { const canQuery = !!debouncedName && debouncedName.length > 0; - // TODO: switch on connected ensnode's configured namespace - const selection = DefaultRecordsSelection.mainnet; + const selection = DefaultRecordsSelection[namespace]; const accelerated = useRecords({ name: debouncedName, diff --git a/apps/ensadmin/src/lib/default-records-selection.ts b/apps/ensadmin/src/lib/default-records-selection.ts index 4c0cc04a65..80e59d99d9 100644 --- a/apps/ensadmin/src/lib/default-records-selection.ts +++ b/apps/ensadmin/src/lib/default-records-selection.ts @@ -4,10 +4,13 @@ import { ENSNamespaceIds, maybeGetDatasource, } from "@ensnode/datasources"; - -import { type CoinType, ETH_COIN_TYPE, evmChainIdToCoinType } from "../ens"; -import { uniq } from "../shared"; -import type { ResolverRecordsSelection } from "./resolver-records-selection"; +import { + CoinType, + ETH_COIN_TYPE, + evmChainIdToCoinType, + ResolverRecordsSelection, + uniq, +} from "@ensnode/ensnode-sdk"; const getENSIP19SupportedCoinTypes = (namespace: ENSNamespaceId) => uniq( @@ -37,11 +40,9 @@ const TEXTS = [ "com.github", ] as const satisfies string[]; -// TODO: Phase out this concept. All apps should define their own selection of records. -// Additionally, we should update `useRecords` so that it can return not only all the -// (texts / addresses) records that are explicitly requested, but also any other (texts / addresses) -// records that ENSNode has found onchain. -// see: https://github.com/namehash/ensnode/issues/1084 +/** + * Defines a set of 'default' records to query when making Protocol Inspector requests. + */ export const DefaultRecordsSelection = { [ENSNamespaceIds.Mainnet]: { addresses: getCommonCoinTypes(ENSNamespaceIds.Mainnet), diff --git a/packages/ensnode-sdk/src/resolution/index.ts b/packages/ensnode-sdk/src/resolution/index.ts index 6e7390288b..9824550cd5 100644 --- a/packages/ensnode-sdk/src/resolution/index.ts +++ b/packages/ensnode-sdk/src/resolution/index.ts @@ -1,4 +1,3 @@ -export * from "./default-records-selection"; export * from "./ensip19-chainid"; export * from "./identity"; export * from "./resolver-records-response"; From 79767922562fd13a01d8144c02f50ccc7f8102be Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 17:39:12 -0500 Subject: [PATCH 10/15] docs(changeset): ENSAdmin now displays whether ENSNode attempted acceleration for an acceleratable endpoint in the Protocol Inspector. --- .changeset/sixty-onions-leave.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sixty-onions-leave.md diff --git a/.changeset/sixty-onions-leave.md b/.changeset/sixty-onions-leave.md new file mode 100644 index 0000000000..542b2cf811 --- /dev/null +++ b/.changeset/sixty-onions-leave.md @@ -0,0 +1,5 @@ +--- +"ensadmin": minor +--- + +ENSAdmin now displays whether ENSNode attempted acceleration for an acceleratable endpoint in the Protocol Inspector. From 9494474d8291bbe650f4175a9f6a0227248de1d0 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 17:42:08 -0500 Subject: [PATCH 11/15] fix: import --- .../src/app/name/_components/NameDetailPageContent.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/ensadmin/src/app/name/_components/NameDetailPageContent.tsx b/apps/ensadmin/src/app/name/_components/NameDetailPageContent.tsx index 40b49e7829..b0b1340ab4 100644 --- a/apps/ensadmin/src/app/name/_components/NameDetailPageContent.tsx +++ b/apps/ensadmin/src/app/name/_components/NameDetailPageContent.tsx @@ -1,10 +1,11 @@ "use client"; import { ASSUME_IMMUTABLE_QUERY, useRecords } from "@ensnode/ensnode-react"; -import { getCommonCoinTypes, type Name, type ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; +import { type Name, type ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; import { Card, CardContent } from "@/components/ui/card"; import { useActiveNamespace } from "@/hooks/active/use-active-namespace"; +import { getCommonCoinTypes } from "@/lib/default-records-selection"; import { AdditionalRecords } from "./AdditionalRecords"; import { Addresses } from "./Addresses"; From 31fefab3934860c4d373bf347123a581b909363b Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 17:50:47 -0500 Subject: [PATCH 12/15] add database to ensapi connection info --- .../connection/config-info/config-info.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/ensadmin/src/components/connection/config-info/config-info.tsx b/apps/ensadmin/src/components/connection/config-info/config-info.tsx index 4ac1cd2318..0aeef8eced 100644 --- a/apps/ensadmin/src/components/connection/config-info/config-info.tsx +++ b/apps/ensadmin/src/components/connection/config-info/config-info.tsx @@ -7,7 +7,7 @@ import { PlugZap, Replace } from "lucide-react"; -import type { ENSApiPublicConfig, ENSIndexerPublicConfig } from "@ensnode/ensnode-sdk"; +import type { ENSApiPublicConfig } from "@ensnode/ensnode-sdk"; import { ChainIcon } from "@/components/chains/ChainIcon"; import { ConfigInfoAppCard } from "@/components/connection/config-info/app-card"; @@ -161,7 +161,24 @@ export function ENSNodeConfigInfo({ {/*ENSApi*/} } + icon={} + items={[ + { + label: "Database", + value:

Postgres

, + }, + { + label: "Database Schema", + value: ( +

+ {ensIndexerPublicConfig.databaseSchemaName} +

+ ), + additionalInfo: ( +

ENSApi reads from tables within this Postgres database schema.

+ ), + }, + ]} version={ensApiPublicConfig.version} docsLink={new URL("https://ensnode.io/ensapi/")} /> From 99eb8cfc3599d7922712993d413aeef5a3ae9ea5 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 17:53:04 -0500 Subject: [PATCH 13/15] add rpc config to ensadmin connection info --- .../connection/config-info/config-info.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/ensadmin/src/components/connection/config-info/config-info.tsx b/apps/ensadmin/src/components/connection/config-info/config-info.tsx index 0aeef8eced..f0ec4a7768 100644 --- a/apps/ensadmin/src/components/connection/config-info/config-info.tsx +++ b/apps/ensadmin/src/components/connection/config-info/config-info.tsx @@ -7,7 +7,7 @@ import { PlugZap, Replace } from "lucide-react"; -import type { ENSApiPublicConfig } from "@ensnode/ensnode-sdk"; +import { type ENSApiPublicConfig, getENSRootChainId } from "@ensnode/ensnode-sdk"; import { ChainIcon } from "@/components/chains/ChainIcon"; import { ConfigInfoAppCard } from "@/components/connection/config-info/app-card"; @@ -119,6 +119,8 @@ export function ENSNodeConfigInfo({

); + const ensRootChainId = getENSRootChainId(ensIndexerPublicConfig.namespace); + return (
{/*ENSAdmin*/} @@ -178,6 +180,24 @@ export function ENSNodeConfigInfo({

ENSApi reads from tables within this Postgres database schema.

), }, + { + label: "RPC Config", + value: ( +
+ + + + + + {getChainName(ensRootChainId)} + + +
+ ), + }, ]} version={ensApiPublicConfig.version} docsLink={new URL("https://ensnode.io/ensapi/")} From 186888ebe29a66331aeccc5f1970f34a8aa81e7c Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 27 Oct 2025 18:18:15 -0500 Subject: [PATCH 14/15] fix: tidy up logging related logic --- apps/ensapi/.env.local.example | 5 ++ apps/ensapi/src/lib/logger.ts | 46 +++++-------- apps/ensapi/vitest.config.ts | 3 - .../src/commands/convert-command.ts | 5 +- .../src/commands/ingest-protobuf-command.ts | 5 +- apps/ensrainbow/src/utils/logger.test.ts | 66 ------------------ apps/ensrainbow/src/utils/logger.ts | 67 +++---------------- apps/ensrainbow/tsconfig.json | 2 + apps/ensrainbow/types/env.d.ts | 7 ++ apps/ensrainbow/vitest.config.ts | 3 - packages/ensnode-sdk/src/internal.ts | 1 + .../ensnode-sdk/src/shared/log-level.test.ts | 27 ++++++++ packages/ensnode-sdk/src/shared/log-level.ts | 21 ++++++ vitest.config.ts | 5 ++ 14 files changed, 101 insertions(+), 162 deletions(-) delete mode 100644 apps/ensrainbow/src/utils/logger.test.ts create mode 100644 apps/ensrainbow/types/env.d.ts create mode 100644 packages/ensnode-sdk/src/shared/log-level.test.ts create mode 100644 packages/ensnode-sdk/src/shared/log-level.ts diff --git a/apps/ensapi/.env.local.example b/apps/ensapi/.env.local.example index aef395b3a6..b967d7624e 100644 --- a/apps/ensapi/.env.local.example +++ b/apps/ensapi/.env.local.example @@ -53,3 +53,8 @@ DATABASE_URL=postgresql://dbuser:abcd1234@localhost:5432/my_database # RPC_URL_11155111=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY # RPC_URL_17000=https://eth-holesky.g.alchemy.com/v2/YOUR_API_KEY # RPC_URL_1337=http://localhost:8545 + +# Log Level +# Optional. If this is not set, the default value is "info". +# Allowed values: "fatal", "error", "warn", "info", "debug", "trace", "silent". +# LOG_LEVEL=info diff --git a/apps/ensapi/src/lib/logger.ts b/apps/ensapi/src/lib/logger.ts index 7f64175d40..50c2642a63 100644 --- a/apps/ensapi/src/lib/logger.ts +++ b/apps/ensapi/src/lib/logger.ts @@ -1,33 +1,19 @@ -import pino, { type Level, levels } from "pino"; -import { prettifyError, z } from "zod/v4"; +import pino from "pino"; -const makePino = (level: Level) => - pino({ - level, - transport: - process.env.NODE_ENV === "production" - ? undefined - : { - target: "pino-pretty", - options: { - colorize: true, - ignore: "pid,hostname", - }, - }, - }); - -const LogLevelSchema = z - .enum(levels.labels, { - error: `Invalid LOG_LEVEL, expected one of '${Object.values(levels.labels).join("' | '")}'`, - }) - .transform((level) => level as Level) - .default("debug"); +import { getLogLevelFromEnv, type LogLevel } from "@ensnode/ensnode-sdk/internal"; -const level = LogLevelSchema.safeParse(process.env.LOG_LEVEL); +const DEFAULT_LOG_LEVEL: LogLevel = "info"; -if (!level.success) { - makePino("fatal").fatal(prettifyError(level.error)); - process.exit(1); -} - -export default makePino(level.data); +export default pino({ + level: getLogLevelFromEnv(process.env, DEFAULT_LOG_LEVEL), + transport: + process.env.NODE_ENV === "production" + ? undefined + : { + target: "pino-pretty", + options: { + colorize: true, + ignore: "pid,hostname", + }, + }, +}); diff --git a/apps/ensapi/vitest.config.ts b/apps/ensapi/vitest.config.ts index f8d6e771c9..3428a80d83 100644 --- a/apps/ensapi/vitest.config.ts +++ b/apps/ensapi/vitest.config.ts @@ -10,8 +10,5 @@ export default defineProject({ }, test: { environment: "node", - env: { - LOG_LEVEL: "fatal", - }, }, }); diff --git a/apps/ensrainbow/src/commands/convert-command.ts b/apps/ensrainbow/src/commands/convert-command.ts index 964026aafc..e48258e6a5 100644 --- a/apps/ensrainbow/src/commands/convert-command.ts +++ b/apps/ensrainbow/src/commands/convert-command.ts @@ -50,7 +50,10 @@ function setupProgressBar(): ProgressBar { incomplete: " ", width: 40, total: 150000000, // estimated - stream: logger.level === "silent" ? createWriteStream("/dev/null") : undefined, + stream: + logger.level === "silent" || logger.level === "fatal" + ? createWriteStream("/dev/null") + : undefined, }, ); } diff --git a/apps/ensrainbow/src/commands/ingest-protobuf-command.ts b/apps/ensrainbow/src/commands/ingest-protobuf-command.ts index 5e0a97d7da..e11e69d3f7 100644 --- a/apps/ensrainbow/src/commands/ingest-protobuf-command.ts +++ b/apps/ensrainbow/src/commands/ingest-protobuf-command.ts @@ -105,7 +105,10 @@ export async function ingestProtobufCommand(options: IngestProtobufCommandOption incomplete: " ", width: 40, total: 1000000000, // Placeholder total - stream: logger.level === "silent" ? createWriteStream("/dev/null") : undefined, + stream: + logger.level === "silent" || logger.level === "fatal" + ? createWriteStream("/dev/null") + : undefined, }, ); diff --git a/apps/ensrainbow/src/utils/logger.test.ts b/apps/ensrainbow/src/utils/logger.test.ts deleted file mode 100644 index 9bceb266fa..0000000000 --- a/apps/ensrainbow/src/utils/logger.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; - -import { - createLogger, - DEFAULT_LOG_LEVEL, - getEnvLogLevel, - parseLogLevel, - VALID_LOG_LEVELS, -} from "./logger"; - -describe("logger", () => { - describe("parseLogLevel", () => { - it("should accept valid log levels", () => { - VALID_LOG_LEVELS.forEach((level) => { - expect(parseLogLevel(level)).toBe(level); - }); - }); - - it("should handle case-insensitive input", () => { - expect(parseLogLevel("INFO")).toBe("info"); - expect(parseLogLevel("Debug")).toBe("debug"); - expect(parseLogLevel("ERROR")).toBe("error"); - }); - - it("should throw error for invalid log level", () => { - expect(() => parseLogLevel("invalid")).toThrow( - 'Invalid log level "invalid". Valid levels are: fatal, error, warn, info, debug, trace, silent', - ); - }); - }); - - describe("getEnvLogLevel", () => { - afterEach(() => { - vi.unstubAllEnvs(); - }); - - it("should return DEFAULT_LOG_LEVEL when LOG_LEVEL is not set", () => { - vi.stubEnv("LOG_LEVEL", undefined); - expect(getEnvLogLevel()).toBe(DEFAULT_LOG_LEVEL); - }); - - it("should return valid log level from environment", () => { - vi.stubEnv("LOG_LEVEL", "debug"); - expect(getEnvLogLevel()).toBe("debug"); - }); - - it("should error when invalid log level in environment", () => { - vi.stubEnv("LOG_LEVEL", "invalid"); - expect(() => getEnvLogLevel()).toThrow( - 'Environment variable error: (LOG_LEVEL): Invalid log level "invalid". Valid levels are: fatal, error, warn, info, debug, trace, silent.', - ); - }); - }); - - describe("createLogger", () => { - it("should create logger with default level when no level provided", () => { - const logger = createLogger(); - expect(logger.level).toBe(DEFAULT_LOG_LEVEL); - }); - - it("should create logger with specified level", () => { - const logger = createLogger("debug"); - expect(logger.level).toBe("debug"); - }); - }); -}); diff --git a/apps/ensrainbow/src/utils/logger.ts b/apps/ensrainbow/src/utils/logger.ts index d6313de784..b7e6f3d1a4 100644 --- a/apps/ensrainbow/src/utils/logger.ts +++ b/apps/ensrainbow/src/utils/logger.ts @@ -1,56 +1,14 @@ -import pino, { type LevelWithSilent } from "pino"; +import pino from "pino"; -import { getErrorMessage } from "@/utils/error-utils"; +import { getLogLevelFromEnv, type LogLevel } from "@ensnode/ensnode-sdk/internal"; -export type LogLevel = LevelWithSilent; +const DEFAULT_LOG_LEVEL: LogLevel = "info"; -export const DEFAULT_LOG_LEVEL: LogLevel = "info"; - -// Creating our own definition of the log levels recognized by Pino -// to provide a better user experience with clear error messages when invalid log levels are -// parsed. -export const VALID_LOG_LEVELS: LogLevel[] = [ - "fatal", - "error", - "warn", - "info", - "debug", - "trace", - "silent", -]; - -export function parseLogLevel(maybeLevel: string): LogLevel { - const normalizedLevel = maybeLevel.toLowerCase(); - if (VALID_LOG_LEVELS.includes(normalizedLevel as LogLevel)) { - return normalizedLevel as LogLevel; - } - throw new Error( - `Invalid log level "${maybeLevel}". Valid levels are: ${VALID_LOG_LEVELS.join(", ")}.`, - ); -} - -export function getEnvLogLevel(): LogLevel { - const envLogLevel = process.env.LOG_LEVEL; - if (!envLogLevel) { - return DEFAULT_LOG_LEVEL; - } - - try { - return parseLogLevel(envLogLevel); - } catch (error: unknown) { - const errorMessage = `Environment variable error: (LOG_LEVEL): ${getErrorMessage(error)}`; - // Log error to console since we can't use logger yet - console.error(errorMessage); - throw new Error(errorMessage); - } -} - -export function createLogger(level: LogLevel = DEFAULT_LOG_LEVEL): pino.Logger { - const isProduction = process.env.NODE_ENV === "production"; - - return pino({ - level, - transport: isProduction +// Create and export the global logger instance +export const logger = pino({ + level: getLogLevelFromEnv(process.env, DEFAULT_LOG_LEVEL), + transport: + process.env.NODE_ENV === "production" ? undefined : { target: "pino-pretty", @@ -59,11 +17,4 @@ export function createLogger(level: LogLevel = DEFAULT_LOG_LEVEL): pino.Logger { ignore: "pid,hostname", }, }, - }); -} - -// Create and export the global logger instance -export const logger = createLogger(getEnvLogLevel()); - -// Re-export pino types for convenience -export type { Logger } from "pino"; +}); diff --git a/apps/ensrainbow/tsconfig.json b/apps/ensrainbow/tsconfig.json index f16245724b..3a25d8b4ba 100644 --- a/apps/ensrainbow/tsconfig.json +++ b/apps/ensrainbow/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "@ensnode/shared-configs/tsconfig.lib.json", "compilerOptions": { + "target": "esnext", + "typeRoots": ["./types"], "paths": { "@/*": ["./src/*"] } diff --git a/apps/ensrainbow/types/env.d.ts b/apps/ensrainbow/types/env.d.ts new file mode 100644 index 0000000000..dd956c6006 --- /dev/null +++ b/apps/ensrainbow/types/env.d.ts @@ -0,0 +1,7 @@ +import type { LogLevelEnvironment } from "@ensnode/ensnode-sdk/internal"; + +declare global { + namespace NodeJS { + interface ProcessEnv extends LogLevelEnvironment {} + } +} diff --git a/apps/ensrainbow/vitest.config.ts b/apps/ensrainbow/vitest.config.ts index 918d3a3fa9..3428a80d83 100644 --- a/apps/ensrainbow/vitest.config.ts +++ b/apps/ensrainbow/vitest.config.ts @@ -10,8 +10,5 @@ export default defineProject({ }, test: { environment: "node", - env: { - LOG_LEVEL: "silent", - }, }, }); diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 31cff5a512..63e0c985b6 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -23,5 +23,6 @@ export * from "./shared/config/types"; export * from "./shared/config/validatons"; export * from "./shared/config/zod-schemas"; export * from "./shared/datasources-with-resolvers"; +export * from "./shared/log-level"; export * from "./shared/protocol-acceleration/interpret-record-values"; export * from "./shared/zod-schemas"; diff --git a/packages/ensnode-sdk/src/shared/log-level.test.ts b/packages/ensnode-sdk/src/shared/log-level.test.ts new file mode 100644 index 0000000000..58fa3516a7 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/log-level.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { LogLevelEnvironment } from "../internal"; +import { getLogLevelFromEnv } from "./log-level"; + +describe("logger", () => { + describe("getLogLevelFromEnv", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("should return default when LOG_LEVEL is not set", () => { + vi.stubEnv("LOG_LEVEL", undefined); + expect(getLogLevelFromEnv(process.env as LogLevelEnvironment, "debug")).toBe("debug"); + }); + + it("should return valid log level from environment", () => { + vi.stubEnv("LOG_LEVEL", "warn"); + expect(getLogLevelFromEnv(process.env as LogLevelEnvironment, "warn")).toBe("warn"); + }); + + it("should return default when invalid log level in environment", () => { + vi.stubEnv("LOG_LEVEL", "invalid"); + expect(getLogLevelFromEnv(process.env as LogLevelEnvironment, "debug")).toBe("debug"); + }); + }); +}); diff --git a/packages/ensnode-sdk/src/shared/log-level.ts b/packages/ensnode-sdk/src/shared/log-level.ts new file mode 100644 index 0000000000..e38c51b954 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/log-level.ts @@ -0,0 +1,21 @@ +import { z } from "zod/v4"; + +import type { LogLevelEnvironment } from "../internal"; + +/** + * Set of valid log levels, mirroring pino#LogLevelWithSilent. + */ +const LogLevelSchema = z.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]); + +export type LogLevel = z.infer; + +export function getLogLevelFromEnv(env: LogLevelEnvironment, defaultLogLevel: LogLevel): LogLevel { + try { + return LogLevelSchema.default(defaultLogLevel).parse(env.LOG_LEVEL); + } catch { + console.warn( + `Invalid LOG_LEVEL '${env.LOG_LEVEL}', expected one of '${Object.values(LogLevelSchema.enum).join("' | '")}' defaulting to '${defaultLogLevel}'`, + ); + return defaultLogLevel; + } +} diff --git a/vitest.config.ts b/vitest.config.ts index 5fc0bdd08a..8ff5bda545 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,5 +3,10 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { projects: ["apps/*/vitest.config.ts", "packages/*/vitest.config.ts"], + // we place LOG_LEVEL here at the root such that running vitest within a specific project continues + // to print logs at the default log level + env: { + LOG_LEVEL: "silent", + }, }, }); From a1955d47ec254f3fa15e0353ff178433862f200c Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 28 Oct 2025 12:26:18 -0500 Subject: [PATCH 15/15] fix: remove unnecessary lazy ensadmin version --- .../src/components/connection/index.tsx | 9 ++++++-- .../src/components/ensadmin-version.tsx | 22 ------------------- .../components/recent-registrations/hooks.ts | 6 +---- apps/ensadmin/src/lib/env.ts | 6 ----- 4 files changed, 8 insertions(+), 35 deletions(-) delete mode 100644 apps/ensadmin/src/components/ensadmin-version.tsx diff --git a/apps/ensadmin/src/components/connection/index.tsx b/apps/ensadmin/src/components/connection/index.tsx index d714f69ca9..c71a88f171 100644 --- a/apps/ensadmin/src/components/connection/index.tsx +++ b/apps/ensadmin/src/components/connection/index.tsx @@ -1,11 +1,12 @@ "use client"; +import packageJson from "@/../package.json" with { type: "json" }; + import { PlugZap } from "lucide-react"; import { ENSNodeConfigInfo } from "@/components/connection/config-info"; import { ConfigInfoAppCard } from "@/components/connection/config-info/app-card"; import { CopyButton } from "@/components/copy-button"; -import { ENSAdminVersion } from "@/components/ensadmin-version"; import { ENSAdminIcon } from "@/components/icons/ensnode-apps/ensadmin-icon"; import { useSelectedConnection } from "@/hooks/active/use-selected-connection"; @@ -26,7 +27,11 @@ export default function ConnectionInfo() { } - version={} + version={ +

+ v{packageJson.version} +

+ } docsLink={new URL("https://ensnode.io/ensadmin/")} /> diff --git a/apps/ensadmin/src/components/ensadmin-version.tsx b/apps/ensadmin/src/components/ensadmin-version.tsx deleted file mode 100644 index 4416b492e8..0000000000 --- a/apps/ensadmin/src/components/ensadmin-version.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -import { Skeleton } from "@/components/ui/skeleton"; -import { ensAdminVersion } from "@/lib/env"; - -export function ENSAdminVersion() { - const [version, setVersion] = useState(null); - - useEffect(() => { - ensAdminVersion().then(setVersion); - }, []); - - if (version === null) { - return ; - } - - return ( - v{version} - ); -} diff --git a/apps/ensadmin/src/components/recent-registrations/hooks.ts b/apps/ensadmin/src/components/recent-registrations/hooks.ts index 2496126768..28c0e2c0b8 100644 --- a/apps/ensadmin/src/components/recent-registrations/hooks.ts +++ b/apps/ensadmin/src/components/recent-registrations/hooks.ts @@ -6,7 +6,6 @@ import { deserializeUnixTimestamp, type Name, type UnixTimestamp } from "@ensnod import { useActiveNamespace } from "@/hooks/active/use-active-namespace"; import { useSelectedConnection } from "@/hooks/active/use-selected-connection"; -import { ensAdminVersion } from "@/lib/env"; import { getNameWrapperAddress } from "@/lib/namespace-utils"; import type { Registration } from "./types"; @@ -126,10 +125,7 @@ async function fetchRecentRegistrations( const response = await fetch(new URL(`/subgraph`, ensNodeUrl), { method: "POST", - headers: { - "content-type": "application/json", - "x-ensadmin-version": await ensAdminVersion(), - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query }), }); diff --git a/apps/ensadmin/src/lib/env.ts b/apps/ensadmin/src/lib/env.ts index e9d5cae92c..8d30c082d8 100644 --- a/apps/ensadmin/src/lib/env.ts +++ b/apps/ensadmin/src/lib/env.ts @@ -125,9 +125,3 @@ export function getServerConnectionLibrary(): HttpHostname[] { return uniqueConnections; } - -export async function ensAdminVersion(): Promise { - const packageJson = await import("@/../package.json"); - - return packageJson.version; -}