diff --git a/apps/ensadmin/src/app/mock/config-info/page.tsx b/apps/ensadmin/src/app/mock/config-info/page.tsx index bc6edc3527..5c6f1da3a2 100644 --- a/apps/ensadmin/src/app/mock/config-info/page.tsx +++ b/apps/ensadmin/src/app/mock/config-info/page.tsx @@ -4,7 +4,8 @@ import { useMemo, useState } from "react"; import { buildEnsNodeStackInfo, - deserializeENSApiPublicConfig, + deserializeEnsApiPublicConfig, + deserializeEnsIndexerPublicConfig, type EnsDbPublicConfig, SerializedENSApiPublicConfig, } from "@ensnode/ensnode-sdk"; @@ -41,14 +42,23 @@ export default function MockConfigPage() { default: try { - const ensApiPublicConfig = deserializeENSApiPublicConfig(mockConfigData[selectedConfig]); + const ensApiPublicConfig = deserializeEnsApiPublicConfig(mockConfigData[selectedConfig]); + const ensIndexerPublicConfig = deserializeEnsIndexerPublicConfig( + mockConfigData[selectedConfig], + ); + const ensRainbowPublicConfig = ensIndexerPublicConfig.ensRainbowPublicConfig; const ensDbPublicConfig = { versionInfo: { postgresql: "18.1", }, } satisfies EnsDbPublicConfig; return { - ensNodeStackInfo: buildEnsNodeStackInfo(ensApiPublicConfig, ensDbPublicConfig), + ensNodeStackInfo: buildEnsNodeStackInfo( + ensApiPublicConfig, + ensDbPublicConfig, + ensIndexerPublicConfig, + ensRainbowPublicConfig, + ), } satisfies ENSNodeConfigInfoViewProps; } catch (error) { const errorMessage = diff --git a/apps/ensadmin/src/app/mock/indexing-status-api.mock.ts b/apps/ensadmin/src/app/mock/indexing-status-api.mock.ts index b23533fbc5..6ff37e16c2 100644 --- a/apps/ensadmin/src/app/mock/indexing-status-api.mock.ts +++ b/apps/ensadmin/src/app/mock/indexing-status-api.mock.ts @@ -58,7 +58,6 @@ const serializedEnsIndexerPublicConfig = { } satisfies SerializedEnsIndexerPublicConfig; export const serializedEnsApiPublicConfig = { - ensIndexerPublicConfig: serializedEnsIndexerPublicConfig, theGraphFallback: { canFallback: true, url: "https://api.thegraph.com/subgraphs/name/ensdomains/ens", diff --git a/apps/ensapi/src/cache/stack-info.cache.ts b/apps/ensapi/src/cache/stack-info.cache.ts index 7bc864bf6f..1fcfc2cad1 100644 --- a/apps/ensapi/src/cache/stack-info.cache.ts +++ b/apps/ensapi/src/cache/stack-info.cache.ts @@ -28,10 +28,22 @@ async function loadEnsNodeStackInfo( return cachedResult.result; } - const ensApiPublicConfig = buildEnsApiPublicConfig(config); + const ensIndexerPublicConfig = await ensDbClient.getEnsIndexerPublicConfig(); + + if (!ensIndexerPublicConfig) { + throw new Error("EnsIndexerPublicConfig is not available in ENSDb"); + } + + const ensApiPublicConfig = buildEnsApiPublicConfig(config, ensIndexerPublicConfig); const ensDbPublicConfig = await ensDbClient.buildEnsDbPublicConfig(); + const ensRainbowPublicConfig = ensIndexerPublicConfig.ensRainbowPublicConfig; - return buildEnsNodeStackInfo(ensApiPublicConfig, ensDbPublicConfig); + return buildEnsNodeStackInfo( + ensApiPublicConfig, + ensDbPublicConfig, + ensIndexerPublicConfig, + ensRainbowPublicConfig, + ); } // lazyProxy defers construction until first use so that this module can be diff --git a/apps/ensapi/src/config/config.schema.test.ts b/apps/ensapi/src/config/config.schema.test.ts index f5b10fb14d..18b153ba64 100644 --- a/apps/ensapi/src/config/config.schema.test.ts +++ b/apps/ensapi/src/config/config.schema.test.ts @@ -155,22 +155,11 @@ describe("buildEnsApiPublicConfig", () => { const mockConfig = { port: ENSApi_DEFAULT_PORT, ensDbUrl: BASE_ENV.ENSDB_URL, - ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG, - namespace: ENSINDEXER_PUBLIC_CONFIG.namespace, ensIndexerSchemaName: ENSINDEXER_PUBLIC_CONFIG.ensIndexerSchemaName, - rpcConfigs: new Map([ - [ - 1, - { - httpRPCs: [new URL(VALID_RPC_URL)], - websocketRPC: undefined, - } satisfies RpcConfig, - ], - ]), referralProgramEditionConfigSetUrl: undefined, }; - const result = buildEnsApiPublicConfig(mockConfig); + const result = buildEnsApiPublicConfig(mockConfig, ENSINDEXER_PUBLIC_CONFIG); expect(result).toStrictEqual({ versionInfo: ensApiVersionInfo, @@ -182,52 +171,16 @@ describe("buildEnsApiPublicConfig", () => { }); }); - it("preserves the complete ENSIndexer public config structure", () => { - const mockConfig = { - port: ENSApi_DEFAULT_PORT, - ensDbUrl: BASE_ENV.ENSDB_URL, - ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG, - namespace: ENSINDEXER_PUBLIC_CONFIG.namespace, - ensIndexerSchemaName: ENSINDEXER_PUBLIC_CONFIG.ensIndexerSchemaName, - rpcConfigs: new Map(), - referralProgramEditionConfigSetUrl: undefined, - }; - - const result = buildEnsApiPublicConfig(mockConfig); - - // Verify that all ENSIndexer public config fields are preserved - expect(result.ensIndexerPublicConfig.namespace).toBe(ENSINDEXER_PUBLIC_CONFIG.namespace); - expect(result.ensIndexerPublicConfig.plugins).toEqual(ENSINDEXER_PUBLIC_CONFIG.plugins); - expect(result.ensIndexerPublicConfig.versionInfo).toEqual(ENSINDEXER_PUBLIC_CONFIG.versionInfo); - expect(result.ensIndexerPublicConfig.indexedChainIds).toEqual( - ENSINDEXER_PUBLIC_CONFIG.indexedChainIds, - ); - expect(result.ensIndexerPublicConfig.isSubgraphCompatible).toBe( - ENSINDEXER_PUBLIC_CONFIG.isSubgraphCompatible, - ); - expect(result.ensIndexerPublicConfig.labelSet).toEqual(ENSINDEXER_PUBLIC_CONFIG.labelSet); - expect(result.ensIndexerPublicConfig.ensIndexerSchemaName).toBe( - ENSINDEXER_PUBLIC_CONFIG.ensIndexerSchemaName, - ); - }); - it("includes the theGraphFallback and redacts api key", () => { const mockConfig = { port: ENSApi_DEFAULT_PORT, ensDbUrl: BASE_ENV.ENSDB_URL, - ensIndexerPublicConfig: { - ...ENSINDEXER_PUBLIC_CONFIG, - plugins: ["subgraph"], - isSubgraphCompatible: true, - }, - namespace: ENSINDEXER_PUBLIC_CONFIG.namespace, ensIndexerSchemaName: ENSINDEXER_PUBLIC_CONFIG.ensIndexerSchemaName, - rpcConfigs: new Map(), referralProgramEditionConfigSetUrl: undefined, theGraphApiKey: "secret-api-key", }; - const result = buildEnsApiPublicConfig(mockConfig); + const result = buildEnsApiPublicConfig(mockConfig, ENSINDEXER_PUBLIC_CONFIG); expect(result.theGraphFallback.canFallback).toBe(true); // discriminate the type... diff --git a/apps/ensapi/src/config/config.schema.ts b/apps/ensapi/src/config/config.schema.ts index 17576766f2..62ef203847 100644 --- a/apps/ensapi/src/config/config.schema.ts +++ b/apps/ensapi/src/config/config.schema.ts @@ -1,23 +1,15 @@ -import pRetry from "p-retry"; import { prettifyError, ZodError, z } from "zod/v4"; -import type { EnsApiPublicConfig } from "@ensnode/ensnode-sdk"; +import type { EnsApiPublicConfig, EnsIndexerPublicConfig } from "@ensnode/ensnode-sdk"; import { - buildRpcConfigsFromEnv, canFallbackToTheGraph, - ENSNamespaceSchema, - invariant_rpcConfigsSpecifiedForRootChain, - makeENSIndexerPublicConfigSchema, OptionalPortNumberSchema, - RpcConfigsSchema, TheGraphApiKeySchema, } from "@ensnode/ensnode-sdk/internal"; import { ENSApi_DEFAULT_PORT } from "@/config/defaults"; import ensDbConfig from "@/config/ensdb-config"; import type { EnsApiEnvironment } from "@/config/environment"; -import { invariant_ensIndexerPublicConfigVersionInfo } from "@/config/validations"; -import { ensDbClient } from "@/lib/ensdb/singleton"; import logger from "@/lib/logger"; import { ensApiVersionInfo } from "@/lib/version-info"; @@ -39,21 +31,15 @@ const ReferralProgramEditionConfigSetUrlSchema = z }) .optional(); -const EnsApiConfigSchema = z - .object({ - port: OptionalPortNumberSchema.default(ENSApi_DEFAULT_PORT), - theGraphApiKey: TheGraphApiKeySchema, - namespace: ENSNamespaceSchema, - rpcConfigs: RpcConfigsSchema, - ensIndexerPublicConfig: makeENSIndexerPublicConfigSchema("ensIndexerPublicConfig"), - referralProgramEditionConfigSetUrl: ReferralProgramEditionConfigSetUrlSchema, +const EnsApiConfigSchema = z.object({ + port: OptionalPortNumberSchema.default(ENSApi_DEFAULT_PORT), + theGraphApiKey: TheGraphApiKeySchema, + referralProgramEditionConfigSetUrl: ReferralProgramEditionConfigSetUrlSchema, - // include the ENSDbConfig params in the EnsApiConfigSchema - ensDbUrl: z.string(), - ensIndexerSchemaName: z.string(), - }) - .check(invariant_rpcConfigsSpecifiedForRootChain) - .check(invariant_ensIndexerPublicConfigVersionInfo); + // include the ENSDbConfig params in the EnsApiConfigSchema + ensDbUrl: z.string(), + ensIndexerSchemaName: z.string(), +}); export type EnsApiConfig = z.infer; @@ -65,39 +51,10 @@ export type EnsApiConfig = z.infer; */ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promise { try { - // TODO: transfer the responsibility of fetching - // the ENSIndexer Public Config to a middleware layer, as per: - // https://github.com/namehash/ensnode/issues/1806 - const ensIndexerPublicConfig = await pRetry( - async () => { - const config = await ensDbClient.getEnsIndexerPublicConfig(); - - if (!config) { - throw new Error("ENSIndexer Public Config not yet available in ENSDb."); - } - - return config; - }, - { - retries: 13, // This allows for a total of over 1 hour of retries with the exponential backoff strategy - onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => { - logger.info( - `ENSIndexer Public Config fetch attempt ${attemptNumber} failed (${error.message}). ${retriesLeft} retries left.`, - ); - }, - }, - ); - - const rpcConfigs = buildRpcConfigsFromEnv(env, ensIndexerPublicConfig.namespace); - return EnsApiConfigSchema.parse({ port: env.PORT, theGraphApiKey: env.THEGRAPH_API_KEY, - ensIndexerPublicConfig, - namespace: ensIndexerPublicConfig.namespace, - rpcConfigs, referralProgramEditionConfigSetUrl: env.REFERRAL_PROGRAM_EDITIONS, - // include the validated ENSDb config values in the parsed EnsApiConfig ensDbUrl: ensDbConfig.ensDbUrl, ensIndexerSchemaName: ensDbConfig.ensIndexerSchemaName, @@ -121,17 +78,21 @@ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promis * @param config - The validated EnsApiConfig object * @returns A complete ENSApiPublicConfig object */ -export function buildEnsApiPublicConfig(config: EnsApiConfig): EnsApiPublicConfig { +export function buildEnsApiPublicConfig( + ensApiConfig: EnsApiConfig, + ensIndexerPublicConfig: EnsIndexerPublicConfig, +): EnsApiPublicConfig { + const { isSubgraphCompatible, namespace } = ensIndexerPublicConfig; + return { versionInfo: ensApiVersionInfo, theGraphFallback: canFallbackToTheGraph({ - namespace: config.namespace, + namespace, + isSubgraphCompatible, // NOTE: very important here that we replace the actual server-side api key with a placeholder // so that it's not sent to clients as part of the `theGraphFallback.url`. The placeholder must // pass validation, of course, but the only validation necessary is that it is a string. - theGraphApiKey: config.theGraphApiKey ? "" : undefined, - isSubgraphCompatible: config.ensIndexerPublicConfig.isSubgraphCompatible, + theGraphApiKey: ensApiConfig.theGraphApiKey ? "" : undefined, }), - ensIndexerPublicConfig: config.ensIndexerPublicConfig, }; } diff --git a/apps/ensapi/src/config/redact.ts b/apps/ensapi/src/config/redact.ts index f21d376d4c..ad4f95f128 100644 --- a/apps/ensapi/src/config/redact.ts +++ b/apps/ensapi/src/config/redact.ts @@ -1,4 +1,4 @@ -import { redactRpcConfigs, redactString, redactUrl } from "@ensnode/ensnode-sdk/internal"; +import { redactString, redactUrl } from "@ensnode/ensnode-sdk/internal"; import type { EnsApiConfig } from "@/config/config.schema"; @@ -8,13 +8,10 @@ import type { EnsApiConfig } from "@/config/config.schema"; export function redactEnsApiConfig(config: EnsApiConfig) { return { port: config.port, - namespace: config.namespace, referralProgramEditionConfigSetUrl: config.referralProgramEditionConfigSetUrl ? redactUrl(config.referralProgramEditionConfigSetUrl) : undefined, - ensIndexerPublicConfig: config.ensIndexerPublicConfig, ensDbUrl: redactString(config.ensDbUrl), - rpcConfigs: redactRpcConfigs(config.rpcConfigs), ensIndexerSchemaName: config.ensIndexerSchemaName, theGraphApiKey: config.theGraphApiKey ? redactString(config.theGraphApiKey) : undefined, }; diff --git a/apps/ensapi/src/handlers/api/explore/name-tokens-api.ts b/apps/ensapi/src/handlers/api/explore/name-tokens-api.ts index 7e5a733771..fdca5331a8 100644 --- a/apps/ensapi/src/handlers/api/explore/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/api/explore/name-tokens-api.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { asInterpretedName, getParentInterpretedName, @@ -22,16 +20,16 @@ import { findRegisteredNameTokensForDomain } from "@/lib/name-tokens/find-name-t import { getIndexedSubregistries } from "@/lib/name-tokens/get-indexed-subregistries"; import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; import { nameTokensApiMiddleware } from "@/middleware/name-tokens.middleware"; +import { + ensureEnsNodeStackInfoAvailable, + stackInfoMiddleware, +} from "@/middleware/stack-info.middleware"; import { getNameTokensRoute } from "./name-tokens-api.routes"; -const app = createApp({ middlewares: [indexingStatusMiddleware, nameTokensApiMiddleware] }); - -// lazyProxy defers construction until first use so that this module can be -// imported without env vars being present (e.g. during OpenAPI generation). -const indexedSubregistries = lazyProxy(() => - getIndexedSubregistries(config.namespace, config.ensIndexerPublicConfig.plugins as PluginName[]), -); +const app = createApp({ + middlewares: [stackInfoMiddleware, indexingStatusMiddleware, nameTokensApiMiddleware], +}); /** * Factory function for creating a 404 Name Tokens Not Indexed error response @@ -48,6 +46,8 @@ const makeNameTokensNotIndexedResponse = ( }); app.openapi(getNameTokensRoute, async (c) => { + ensureEnsNodeStackInfoAvailable(c); + // Check if Indexing Status resolution failed. if (c.var.indexingStatus instanceof Error) { return c.json( @@ -84,6 +84,14 @@ app.openapi(getNameTokensRoute, async (c) => { } const parentNode = namehashInterpretedName(parentName); + const { namespace, plugins } = c.var.stackInfo.ensIndexer; + + // lazyProxy defers construction until first use so that this module can be + // imported without env vars being present (e.g. during OpenAPI generation). + const indexedSubregistries = lazyProxy(() => + getIndexedSubregistries(namespace, plugins as PluginName[]), + ); + const subregistry = indexedSubregistries.find((s) => s.node === parentNode); // Return 404 response with error code for Name Tokens Not Indexed when @@ -111,7 +119,12 @@ app.openapi(getNameTokensRoute, async (c) => { const { omnichainSnapshot } = c.var.indexingStatus.snapshot; const accurateAsOf = omnichainSnapshot.omnichainIndexingCursor; - const registeredNameTokens = await findRegisteredNameTokensForDomain(domainId, accurateAsOf); + const { namespace } = c.var.stackInfo.ensIndexer; + const registeredNameTokens = await findRegisteredNameTokensForDomain( + namespace, + domainId, + accurateAsOf, + ); // Return 404 response with error code for Name Tokens Not Indexed when // no name tokens were found for the domain ID associated with diff --git a/apps/ensapi/src/handlers/api/meta/status-api.ts b/apps/ensapi/src/handlers/api/meta/status-api.ts index f2db1b4515..dacfebcbc0 100644 --- a/apps/ensapi/src/handlers/api/meta/status-api.ts +++ b/apps/ensapi/src/handlers/api/meta/status-api.ts @@ -7,14 +7,18 @@ import { import { createApp } from "@/lib/hono-factory"; import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; -import { stackInfoMiddleware } from "@/middleware/stack-info.middleware"; +import { + ensureEnsNodeStackInfoAvailable, + stackInfoMiddleware, +} from "@/middleware/stack-info.middleware"; import { getIndexingStatusRoute } from "./status-api.routes"; const app = createApp({ middlewares: [stackInfoMiddleware, indexingStatusMiddleware] }); app.openapi(getIndexingStatusRoute, async (c) => { - if (c.var.indexingStatus instanceof Error || c.var.stackInfo instanceof Error) { + ensureEnsNodeStackInfoAvailable(c); + if (c.var.indexingStatus instanceof Error) { return c.json( serializeEnsApiIndexingStatusResponse({ responseCode: EnsApiIndexingStatusResponseCodes.Error, diff --git a/apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts b/apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts index 31fbf3d292..4f94d03ac6 100644 --- a/apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts +++ b/apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts @@ -1,14 +1,15 @@ -import config from "@/config"; - import { hasOmnigraphApiConfigSupport } from "@ensnode/ensnode-sdk"; import { createApp } from "@/lib/hono-factory"; +import { ensureEnsNodeStackInfoAvailable } from "@/middleware/stack-info.middleware"; const app = createApp(); // 503 if prerequisites not met app.use(async (c, next) => { - const prerequisite = hasOmnigraphApiConfigSupport(config.ensIndexerPublicConfig); + ensureEnsNodeStackInfoAvailable(c); + const ensIndexerPublicConfig = c.var.stackInfo.ensIndexer; + const prerequisite = hasOmnigraphApiConfigSupport(ensIndexerPublicConfig); if (!prerequisite.supported) { return c.text(`Service Unavailable: ${prerequisite.reason}`, 503); } @@ -17,10 +18,13 @@ app.use(async (c, next) => { }); app.use(async (c) => { + ensureEnsNodeStackInfoAvailable(c); + const { namespace } = c.var.stackInfo.ensIndexer; // defer the loading of the GraphQL Server until runtime, which allows these modules to require // the Namechain datasource // TODO(ensv2): this can be removed if/when all ENSNamespaces define the Namechain Datasource - const { yoga } = await import("@/omnigraph-api/yoga"); + const { createYogaForNamespace } = await import("@/omnigraph-api/yoga"); + const yoga = createYogaForNamespace(namespace); return yoga.fetch(c.req.raw, c.var); }); diff --git a/apps/ensapi/src/handlers/api/resolution/resolution-api.ts b/apps/ensapi/src/handlers/api/resolution/resolution-api.ts index edfce56114..5a5e85f0ea 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolution-api.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolution-api.ts @@ -14,6 +14,7 @@ import { runWithTrace } from "@/lib/tracing/tracing-api"; import { canAccelerateMiddleware } from "@/middleware/can-accelerate.middleware"; import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; import { makeIsRealtimeMiddleware } from "@/middleware/is-realtime.middleware"; +import { ensureEnsNodeStackInfoAvailable } from "@/middleware/stack-info.middleware"; import { resolvePrimaryNameRoute, @@ -48,12 +49,14 @@ const app = createApp({ * GET /records/example.eth&name=true&addresses=60,0&texts=avatar,com.twitter */ app.openapi(resolveRecordsRoute, async (c) => { + ensureEnsNodeStackInfoAvailable(c); const { name } = c.req.valid("param"); const { selection, trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; + const { namespace, plugins } = c.var.stackInfo.ensIndexer; const { result, trace } = await runWithTrace(() => - resolveForward(name, selection, { accelerate, canAccelerate }), + resolveForward(namespace, plugins, name, selection, { accelerate, canAccelerate }), ); const response = { @@ -80,12 +83,14 @@ app.openapi(resolveRecordsRoute, async (c) => { * GET /primary-name/0x1234...abcd/0 */ app.openapi(resolvePrimaryNameRoute, async (c) => { + ensureEnsNodeStackInfoAvailable(c); const { address, chainId } = c.req.valid("param"); const { trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; + const { namespace, plugins } = c.var.stackInfo.ensIndexer; const { result, trace } = await runWithTrace(() => - resolveReverse(address, chainId, { accelerate, canAccelerate }), + resolveReverse(namespace, plugins, address, chainId, { accelerate, canAccelerate }), ); const response = { @@ -109,12 +114,14 @@ app.openapi(resolvePrimaryNameRoute, async (c) => { * GET /primary-names/0x1234...abcd?chainIds=1,10,8453 */ app.openapi(resolvePrimaryNamesRoute, async (c) => { + ensureEnsNodeStackInfoAvailable(c); const { address } = c.req.valid("param"); const { chainIds, trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; + const { namespace, plugins } = c.var.stackInfo.ensIndexer; const { result, trace } = await runWithTrace(() => - resolvePrimaryNames(address, chainIds, { accelerate, canAccelerate }), + resolvePrimaryNames(namespace, plugins, address, chainIds, { accelerate, canAccelerate }), ); const response = { diff --git a/apps/ensapi/src/handlers/api/router.ts b/apps/ensapi/src/handlers/api/router.ts index 1b15e11f1c..4822869ef3 100644 --- a/apps/ensapi/src/handlers/api/router.ts +++ b/apps/ensapi/src/handlers/api/router.ts @@ -1,4 +1,5 @@ import { createApp } from "@/lib/hono-factory"; +import { stackInfoMiddleware } from "@/middleware/stack-info.middleware"; import nameTokensApi from "./explore/name-tokens-api"; import registrarActionsApi from "./explore/registrar-actions-api"; @@ -7,7 +8,9 @@ import statusApi from "./meta/status-api"; import omnigraphApi from "./omnigraph/omnigraph-api"; import resolutionApi from "./resolution/resolution-api"; -const app = createApp(); +const app = createApp({ + middlewares: [stackInfoMiddleware], +}); app.route("/", statusApi); app.route("/realtime", realtimeApi); diff --git a/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts b/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts index 5df6b40a29..e078bb8e03 100644 --- a/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts +++ b/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts @@ -1,21 +1,8 @@ import { describe, expect, it, vi } from "vitest"; -import { ENSNamespaceIds } from "@ensnode/datasources"; - -import type { EnsApiConfig } from "@/config/config.schema"; import * as editionsCachesMiddleware from "@/middleware/referral-leaderboard-editions-caches.middleware"; import * as editionSetMiddleware from "@/middleware/referral-program-edition-set.middleware"; -vi.mock("@/config", () => ({ - get default() { - const mockedConfig: Pick = { - namespace: ENSNamespaceIds.Mainnet, - }; - - return mockedConfig; - }, -})); - vi.mock("@/middleware/referral-program-edition-set.middleware", () => ({ referralProgramEditionConfigSetMiddleware: vi.fn(), })); diff --git a/apps/ensapi/src/handlers/subgraph/subgraph-api.ts b/apps/ensapi/src/handlers/subgraph/subgraph-api.ts index b0b01929c1..3955d6c1d8 100644 --- a/apps/ensapi/src/handlers/subgraph/subgraph-api.ts +++ b/apps/ensapi/src/handlers/subgraph/subgraph-api.ts @@ -18,6 +18,10 @@ import { filterSchemaByPrefix } from "@/lib/subgraph/filter-schema-by-prefix"; import { fixContentLengthMiddleware } from "@/middleware/fix-content-length.middleware"; import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; import { makeIsRealtimeMiddleware } from "@/middleware/is-realtime.middleware"; +import { + ensureEnsNodeStackInfoAvailable, + stackInfoMiddleware, +} from "@/middleware/stack-info.middleware"; import { subgraphMetaMiddleware } from "@/middleware/subgraph-meta.middleware"; import { thegraphFallbackMiddleware } from "@/middleware/thegraph-fallback.middleware"; @@ -26,11 +30,15 @@ const MAX_REALTIME_DISTANCE_TO_RESOLVE: Duration = 10 * 60; // 10 minutes in sec // generate a subgraph-specific subset of the schema const subgraphSchema = filterSchemaByPrefix("subgraph_", ensIndexerSchema); -const app = createApp(); +const app = createApp({ middlewares: [stackInfoMiddleware] }); // 503 if subgraph plugin not available app.use(async (c, next) => { - const prerequisite = hasSubgraphApiConfigSupport(config.ensIndexerPublicConfig); + ensureEnsNodeStackInfoAvailable(c); + + const ensIndexerPublicConfig = c.var.stackInfo.ensIndexer; + + const prerequisite = hasSubgraphApiConfigSupport(ensIndexerPublicConfig); if (!prerequisite.supported) { return c.text(`Service Unavailable: ${prerequisite.reason}`, 503); } diff --git a/apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts b/apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts index 3b4b6ca956..c4800c396d 100644 --- a/apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts +++ b/apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts @@ -1,10 +1,9 @@ -import config from "@/config"; - import { eq } from "drizzle-orm/sql"; import { type AccountId, asInterpretedName, type Node, type UnixTimestamp } from "enssdk"; import { bigIntToNumber, + type ENSNamespaceId, getNameTokenOwnership, type NameToken, type NameTokenOwnership, @@ -80,6 +79,7 @@ function _recordToNameToken( * domain ID were found. Otherwise returns null. */ function _recordsToRegisteredNameTokens( + namespace: ENSNamespaceId, domainId: Node, records: FindRegisteredNameTokensForDomainRecord[], accurateAsOf: UnixTimestamp, @@ -103,7 +103,7 @@ function _recordsToRegisteredNameTokens( } satisfies AccountId; // biome-ignore lint/style/noNonNullAssertion: domain.name guaranteed to exist const name = asInterpretedName(record.domains.name!); - const ownership = getNameTokenOwnership(config.namespace, name, owner); + const ownership = getNameTokenOwnership(namespace, name, owner); const token = _recordToNameToken(record, ownership); const expiresAt = bigIntToNumber(record.registrationLifecycles.expiresAt); @@ -144,10 +144,11 @@ function _recordsToRegisteredNameTokens( * the name associated with the domainId is not an actively indexed subregistry. */ export async function findRegisteredNameTokensForDomain( + namespace: ENSNamespaceId, domainId: Node, accurateAsOf: UnixTimestamp, ): Promise { const records = await _findRegisteredNameTokensForDomain(domainId); - return _recordsToRegisteredNameTokens(domainId, records, accurateAsOf); + return _recordsToRegisteredNameTokens(namespace, domainId, records, accurateAsOf); } diff --git a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts index 31ada4c3fc..aefd40a304 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { bytesToPacket } from "@ensdomains/ensjs/utils"; import { SpanStatusCode, trace } from "@opentelemetry/api"; import { @@ -14,12 +12,11 @@ import { import { isAddressEqual, type PublicClient, toHex, zeroAddress } from "viem"; import { packetToBytes } from "viem/ens"; -import { DatasourceNames, getDatasource } from "@ensnode/datasources"; +import { DatasourceNames, type ENSNamespaceId, getDatasource } from "@ensnode/datasources"; import { accountIdEqual, getDatasourceContract, isENSv1Registry } from "@ensnode/ensnode-sdk"; import { ensDb } from "@/lib/ensdb/singleton"; import { withActiveSpanAsync, withSpanAsync } from "@/lib/instrumentation/auto-span"; -import { lazyProxy } from "@/lib/lazy"; type FindResolverResult = | { @@ -37,12 +34,6 @@ const NULL_RESULT: FindResolverResult = { const tracer = trace.getTracer("find-resolver"); -// lazyProxy defers construction until first use so that this module can be -// imported without env vars being present (e.g. during OpenAPI generation). -const ensv1RegistryOld = lazyProxy(() => - getDatasourceContract(config.namespace, DatasourceNames.ENSRoot, "ENSv1RegistryOld"), -); - /** * Identifies `name`'s active resolver in `registry`. * @@ -53,12 +44,14 @@ const ensv1RegistryOld = lazyProxy(() => * - TODO: any ENSv2 Registry */ export async function findResolver({ + namespace, registry, name, accelerate, canAccelerate, publicClient, }: { + namespace: ENSNamespaceId; registry: AccountId; name: InterpretedName; accelerate: boolean; @@ -73,24 +66,25 @@ export async function findResolver({ // then we can identify a node's active resolver via the indexed Domain-Resolver Relationships. ////////////////////////////////////////////////// if (accelerate && canAccelerate) { - return findResolverWithIndex(registry, name); + return findResolverWithIndex(namespace, registry, name); } // Invariant: UniversalResolver#findResolver only works for ENS Root Registry - if (!isENSv1Registry(config.namespace, registry)) { + if (!isENSv1Registry(namespace, registry)) { throw new Error( `Invariant(findResolver): UniversalResolver#findResolver only identifies active resolvers agains the ENs Root Registry, but a different Registry contract was passed: ${JSON.stringify(registry)}.`, ); } // query the UniversalResolver on the ENSRoot Chain (via RPC) - return findResolverWithUniversalResolver(publicClient, name); + return findResolverWithUniversalResolver(namespace, publicClient, name); } /** * Queries the resolverAddress for the specified `name` using the UniversalResolver via RPC. */ async function findResolverWithUniversalResolver( + namespace: ENSNamespaceId, publicClient: PublicClient, name: InterpretedName, ): Promise { @@ -104,7 +98,7 @@ async function findResolverWithUniversalResolver( contracts: { UniversalResolver: { address, abi }, }, - } = getDatasource(config.namespace, DatasourceNames.ENSRoot); + } = getDatasource(namespace, DatasourceNames.ENSRoot); // 2. Call UniversalResolver#findResolver via RPC const dnsEncodedNameBytes = packetToBytes(name); @@ -173,6 +167,7 @@ async function findResolverWithUniversalResolver( * ``` */ async function findResolverWithIndex( + namespace: ENSNamespaceId, registry: AccountId, name: InterpretedName, ): Promise { @@ -196,6 +191,12 @@ async function findResolverWithIndex( const nodes = names.map((name) => namehashInterpretedName(name)); const domainIds = nodes as DomainId[]; + const ensv1RegistryOld = getDatasourceContract( + namespace, + DatasourceNames.ENSRoot, + "ENSv1RegistryOld", + ); + // 3. for each domain, find its associated resolver in the selected registry const domainResolverRelations = await withSpanAsync( tracer, @@ -213,7 +214,7 @@ async function findResolverWithIndex( // filter for Domain-Resolver Relationship in the current Registry and(eq(t.chainId, registry.chainId), eq(t.address, registry.address)), // OR, if the registry is the ENS Root Registry, also include records from RegistryOld - isENSv1Registry(config.namespace, registry) + isENSv1Registry(namespace, registry) ? and( eq(t.chainId, ensv1RegistryOld.chainId), eq(t.address, ensv1RegistryOld.address), diff --git a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts index d09e930b84..ca42fcd1fd 100644 --- a/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts +++ b/apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts @@ -1,8 +1,6 @@ -import config from "@/config"; - import { type AccountId, DEFAULT_EVM_COIN_TYPE, type Node } from "enssdk"; -import type { ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; +import type { ENSNamespaceId, ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; import { staticResolverImplementsAddressRecordDefaulting } from "@ensnode/ensnode-sdk/internal"; import { ensDb } from "@/lib/ensdb/singleton"; @@ -11,10 +9,12 @@ import type { IndexedResolverRecords } from "@/lib/resolution/make-records-respo const DEFAULT_EVM_COIN_TYPE_BIGINT = BigInt(DEFAULT_EVM_COIN_TYPE); export async function getRecordsFromIndex({ + namespace, resolver, node, selection, }: { + namespace: ENSNamespaceId; resolver: AccountId; node: Node; selection: SELECTION; @@ -36,7 +36,7 @@ export async function getRecordsFromIndex(); /** * Gets a viem#PublicClient for the specified `chainId` using the ENSApiConfig's RPCConfig. Caches * the instance itself to minimize unnecessary allocations. */ -export function getPublicClient(chainId: ChainId): PublicClient { - // Invariant: ENSApi must have an rpcConfig for the requested `chainId` - const rpcConfig = config.rpcConfigs.get(chainId); +export function buildPublicClientForRootChain(namespace: ENSNamespaceId): PublicClient { + const rootChainId = getENSRootChainId(namespace); + const unvalidatedRpcConfigs = buildRpcConfigsFromEnv(config, namespace); + const rpcConfigs = RpcConfigsSchema.parse(unvalidatedRpcConfigs); + const rpcConfig = rpcConfigs.get(rootChainId); + if (!rpcConfig) { - throw new Error(`Invariant: ENSApi does not have an RPC to chain id '${chainId}'.`); + throw new Error(`Invariant: ENSApi does not have an RPC to chain id '${rootChainId}'.`); } - if (!_cache.has(chainId)) { + if (!_cache.has(rootChainId)) { _cache.set( - chainId, + rootChainId, // Create an viem#PublicClient that uses a fallback() transport with all specified HTTP RPCs createPublicClient({ transport: fallback(rpcConfig.httpRPCs.map((url) => http(url.toString()))), @@ -26,7 +32,7 @@ export function getPublicClient(chainId: ChainId): PublicClient { ); } - const publicClient = _cache.get(chainId); + const publicClient = _cache.get(rootChainId); // publicClient guaranteed to exist due to cache-setting logic above if (!publicClient) throw new Error("never"); diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index 2a14b6a6df..7e01cf7eda 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { trace } from "@opentelemetry/api"; import { replaceBigInts } from "@ponder/utils"; import { @@ -14,6 +12,7 @@ import { } from "enssdk"; import { + type ENSNamespaceId, type ForwardResolutionArgs, ForwardResolutionProtocolStep, type ForwardResolutionResult, @@ -37,7 +36,7 @@ import { findResolver } from "@/lib/protocol-acceleration/find-resolver"; import { getENSIP19ReverseNameRecordFromIndex } from "@/lib/protocol-acceleration/get-primary-name-from-index"; import { getRecordsFromIndex } from "@/lib/protocol-acceleration/get-records-from-index"; import { areResolverRecordsIndexedByProtocolAccelerationPluginOnChainId } from "@/lib/protocol-acceleration/resolver-records-indexed-on-chain"; -import { getPublicClient } from "@/lib/public-client"; +import { buildPublicClientForRootChain } from "@/lib/public-client"; import { makeEmptyResolverRecordsResponse, makeRecordsResponseFromIndexedRecords, @@ -88,18 +87,20 @@ const tracer = trace.getTracer("forward-resolution"); * } */ export async function resolveForward( + namespace: ENSNamespaceId, + plugins: string[], name: ForwardResolutionArgs["name"], selection: ForwardResolutionArgs["selection"], - options: Omit[2], "registry">, + options: Omit[4], "registry">, ): Promise> { // Invariant: Name must be an InterpretedName const interpretedName = asInterpretedName(name); // NOTE: `resolveForward` is just `_resolveForward` with the enforcement that `registry` must // initially be ENS Root Registry: see `_resolveForward` for additional context. - return _resolveForward(interpretedName, selection, { + return _resolveForward(namespace, plugins, interpretedName, selection, { ...options, - registry: getENSv1Registry(config.namespace), + registry: getENSv1Registry(namespace), }); } @@ -108,6 +109,8 @@ export async function resolveForward * `registry`. */ async function _resolveForward( + namespace: ENSNamespaceId, + plugins: string[], name: InterpretedName, selection: ForwardResolutionArgs["selection"], options: { registry: AccountId; accelerate: boolean; canAccelerate: boolean }, @@ -175,13 +178,13 @@ async function _resolveForward( ); } - const publicClient = getPublicClient(chainId); + const publicClient = buildPublicClientForRootChain(namespace); //////////////////////////// /// Temporary ENSv2 Bailout //////////////////////////// // TODO: re-enable protocol acceleration for ENSv2 - if (config.ensIndexerPublicConfig.plugins.includes(PluginName.ENSv2)) { + if (plugins.includes(PluginName.ENSv2)) { // execute each record's call against the UniversalResolverV2 const rawResults = await withEnsProtocolStep( TraceableENSProtocol.ForwardResolution, @@ -189,6 +192,7 @@ async function _resolveForward( {}, () => executeResolveCallsWithUniversalResolver({ + namespace, name, calls, publicClient, @@ -218,6 +222,7 @@ async function _resolveForward( { name, chainId }, () => findResolver({ + namespace, registry: options.registry, name, accelerate, @@ -267,7 +272,7 @@ async function _resolveForward( // If the activeResolver is a Known ENSIP-19 Reverse Resolver, // then we can just read the name record value directly from the index. ////////////////////////////////////////////////// - if (isKnownENSIP19ReverseResolver(config.namespace, resolver)) { + if (isKnownENSIP19ReverseResolver(namespace, resolver)) { return withEnsProtocolStep( TraceableENSProtocol.ForwardResolution, ForwardResolutionProtocolStep.AccelerateENSIP19ReverseResolver, @@ -314,13 +319,17 @@ async function _resolveForward( // If the activeResolver is a Bridged Resolver, // then we can short-circuit the CCIP-Read and defer resolution to the indicated (shadow)Registry. ////////////////////////////////////////////////// - const bridgesTo = isBridgedResolver(config.namespace, resolver); + const bridgesTo = isBridgedResolver(namespace, resolver); if (bridgesTo) { return withEnsProtocolStep( TraceableENSProtocol.ForwardResolution, ForwardResolutionProtocolStep.AccelerateKnownOffchainLookupResolver, {}, - () => _resolveForward(name, selection, { ...options, registry: bridgesTo }), + () => + _resolveForward(namespace, plugins, name, selection, { + ...options, + registry: bridgesTo, + }), ); } @@ -340,18 +349,16 @@ async function _resolveForward( // then we can retrieve records directly from the database. ////////////////////////////////////////////////// const resolverRecordsAreIndexed = - areResolverRecordsIndexedByProtocolAccelerationPluginOnChainId( - config.namespace, - chainId, - ); + areResolverRecordsIndexedByProtocolAccelerationPluginOnChainId(namespace, chainId); - if (resolverRecordsAreIndexed && isStaticResolver(config.namespace, resolver)) { + if (resolverRecordsAreIndexed && isStaticResolver(namespace, resolver)) { return withEnsProtocolStep( TraceableENSProtocol.ForwardResolution, ForwardResolutionProtocolStep.AccelerateKnownOnchainStaticResolver, {}, async () => { const records = await getRecordsFromIndex({ + namespace, resolver: { chainId, address: activeResolver }, node, selection, diff --git a/apps/ensapi/src/lib/resolution/multichain-primary-name-resolution.ts b/apps/ensapi/src/lib/resolution/multichain-primary-name-resolution.ts index 930b0142e9..26e7a21a2f 100644 --- a/apps/ensapi/src/lib/resolution/multichain-primary-name-resolution.ts +++ b/apps/ensapi/src/lib/resolution/multichain-primary-name-resolution.ts @@ -1,10 +1,7 @@ -import config from "@/config"; - import { trace } from "@opentelemetry/api"; -import type { ChainId } from "enssdk"; import { mainnet } from "viem/chains"; -import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; +import { DatasourceNames, type ENSNamespaceId, maybeGetDatasource } from "@ensnode/datasources"; import { type MultichainPrimaryNameResolutionArgs, type MultichainPrimaryNameResolutionResult, @@ -12,12 +9,11 @@ import { } from "@ensnode/ensnode-sdk"; import { withActiveSpanAsync } from "@/lib/instrumentation/auto-span"; -import { lazy } from "@/lib/lazy"; import { resolveReverse } from "@/lib/resolution/reverse-resolution"; const tracer = trace.getTracer("multichain-primary-name-resolution"); -const getENSIP19SupportedChainIds = lazy(() => [ +const getENSIP19SupportedChainIds = (namespace: ENSNamespaceId) => [ // always include Mainnet, because its chainId corresponds to the ENS Root Chain's coinType, // regardless of the current namespace mainnet.id, @@ -25,23 +21,25 @@ const getENSIP19SupportedChainIds = lazy(() => [ // then include any ENSIP-19 Supported Chains defined in this namespace ...uniq( [ - maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverRoot), - maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverBase), - maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverLinea), - maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverOptimism), - maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverArbitrum), - maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverScroll), + maybeGetDatasource(namespace, DatasourceNames.ReverseResolverRoot), + maybeGetDatasource(namespace, DatasourceNames.ReverseResolverBase), + maybeGetDatasource(namespace, DatasourceNames.ReverseResolverLinea), + maybeGetDatasource(namespace, DatasourceNames.ReverseResolverOptimism), + maybeGetDatasource(namespace, DatasourceNames.ReverseResolverArbitrum), + maybeGetDatasource(namespace, DatasourceNames.ReverseResolverScroll), ] .filter((ds) => ds !== undefined) .map((ds) => ds.chain.id), ), -]); +]; /** * Implements batch resolution of an address' Primary Name across the provided `chainIds`. * * @see https://docs.ens.domains/ensip/19 * + * @param namespace the ENS namespace within which to resolve the address' Primary Names + * @param plugins the set of plugins to use for resolution * @param address the adddress whose Primary Names to resolve * @param chainIds the set of chainIds within which to resolve the address' Primary Name (default: * all ENSIP-19 supported chains) @@ -50,17 +48,23 @@ const getENSIP19SupportedChainIds = lazy(() => [ * @param options.canAccelerate Whether acceleration is currently possible (default: false) */ export async function resolvePrimaryNames( + namespace: ENSNamespaceId, + plugins: string[], address: MultichainPrimaryNameResolutionArgs["address"], - chainIds: MultichainPrimaryNameResolutionArgs["chainIds"] = getENSIP19SupportedChainIds(), - options: Parameters[2], + chainIds: MultichainPrimaryNameResolutionArgs["chainIds"], + options: Parameters[4], ): Promise { + const _chainIds = chainIds ?? getENSIP19SupportedChainIds(namespace); + // parallel reverseResolve const names = await withActiveSpanAsync(tracer, "resolvePrimaryNames", { address }, () => - Promise.all(chainIds.map((chainId) => resolveReverse(address, chainId, options))), + Promise.all( + _chainIds.map((chainId) => resolveReverse(namespace, plugins, address, chainId, options)), + ), ); // key results by chainId - return chainIds.reduce((memo, chainId, i) => { + return _chainIds.reduce((memo, chainId, i) => { // biome-ignore lint/style/noNonNullAssertion: names[i] guaranteed to be defined memo[chainId] = names[i]!; return memo; diff --git a/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.integration.test.ts b/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.integration.test.ts index 36f46ad586..47e7c33c4d 100644 --- a/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.integration.test.ts +++ b/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.integration.test.ts @@ -22,7 +22,7 @@ import { } from "enssdk"; import { describe, expect, it } from "vitest"; -import { getPublicClient } from "@/lib/public-client"; +import { buildPublicClientForRootChain } from "@/lib/public-client"; import { makeResolveCalls } from "@/lib/resolution/resolve-calls-and-results"; import { executeResolveCallsWithUniversalResolver } from "./resolve-with-universal-resolver"; @@ -35,12 +35,13 @@ const NAME_WITH_ENCODED_LABELHASHES = interpretedLabelsToInterpretedName([ const EXPECTED_DESCRIPTION = "example.eth"; -const publicClient = getPublicClient(ensTestEnvChain.id); +const publicClient = buildPublicClientForRootChain(ENSNamespaceIds.EnsTestEnv); describe("executeResolveCallsWithUniversalResolver", () => { it("should resolve interpreted name without encoded labelhashes", async () => { await expect( executeResolveCallsWithUniversalResolver({ + namespace: ENSNamespaceIds.EnsTestEnv, name: NAME, calls: makeResolveCalls(namehashInterpretedName(NAME), { texts: ["description"] }), publicClient, @@ -62,6 +63,7 @@ describe("executeResolveCallsWithUniversalResolver", () => { it("should NOT resolve interpreted name with encoded labelhashes", async () => { await expect( executeResolveCallsWithUniversalResolver({ + namespace: ENSNamespaceIds.EnsTestEnv, name: NAME_WITH_ENCODED_LABELHASHES, calls: makeResolveCalls(namehashInterpretedName(NAME_WITH_ENCODED_LABELHASHES), { texts: ["description"], diff --git a/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts b/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts index 0965ce984f..054a5502a4 100644 --- a/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts +++ b/apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import type { InterpretedName } from "enssdk"; import { bytesToHex, @@ -12,41 +10,51 @@ import { } from "viem"; import { packetToBytes } from "viem/ens"; -import { DatasourceNames, ResolverABI, UniversalResolverABI } from "@ensnode/datasources"; +import { + DatasourceNames, + type ENSNamespaceId, + ResolverABI, + UniversalResolverABI, +} from "@ensnode/datasources"; import { getDatasourceContract, maybeGetDatasourceContract, type ResolverRecordsSelection, } from "@ensnode/ensnode-sdk"; -import { lazy } from "@/lib/lazy"; import type { ResolveCalls, ResolveCallsAndRawResults, } from "@/lib/resolution/resolve-calls-and-results"; -const getUniversalResolverV1 = lazy(() => - getDatasourceContract(config.namespace, DatasourceNames.ENSRoot, "UniversalResolver"), -); - -const getUniversalResolverV2 = lazy(() => - maybeGetDatasourceContract(config.namespace, DatasourceNames.ENSRoot, "UniversalResolverV2"), -); - /** * Execute a set of ResolveCalls for `name` against the UniversalResolver. */ export async function executeResolveCallsWithUniversalResolver< SELECTION extends ResolverRecordsSelection, >({ + namespace, name, calls, publicClient, }: { + namespace: ENSNamespaceId; name: InterpretedName; calls: ResolveCalls; publicClient: PublicClient; }): Promise> { + const getUniversalResolverV1 = getDatasourceContract( + namespace, + DatasourceNames.ENSRoot, + "UniversalResolver", + ); + + const getUniversalResolverV2 = maybeGetDatasourceContract( + namespace, + DatasourceNames.ENSRoot, + "UniversalResolverV2", + ); + // NOTE: automatically multicalled by viem return await Promise.all( calls.map(async (call) => { @@ -58,7 +66,7 @@ export async function executeResolveCallsWithUniversalResolver< abi: UniversalResolverABI, // NOTE(ensv2-transition): if UniversalResolverV2 is defined, prefer it over UniversalResolver // TODO(ensv2-transition): confirm this is correct - address: getUniversalResolverV2()?.address ?? getUniversalResolverV1().address, + address: getUniversalResolverV2?.address ?? getUniversalResolverV1.address, functionName: "resolve", args: [encodedName, encodedMethod], }); diff --git a/apps/ensapi/src/lib/resolution/reverse-resolution.ts b/apps/ensapi/src/lib/resolution/reverse-resolution.ts index 677d16188f..6622f9a558 100644 --- a/apps/ensapi/src/lib/resolution/reverse-resolution.ts +++ b/apps/ensapi/src/lib/resolution/reverse-resolution.ts @@ -3,6 +3,7 @@ import { coinTypeReverseLabel, evmChainIdToCoinType, reverseName } from "enssdk" import { isAddress, isAddressEqual } from "viem"; import { + type ENSNamespaceId, type ResolverRecordsSelection, type ReverseResolutionArgs, ReverseResolutionProtocolStep, @@ -38,9 +39,11 @@ const tracer = trace.getTracer("reverse-resolution"); * @param options.canAccelerate Whether acceleration is currently possible (default: false) */ export async function resolveReverse( + namespace: ENSNamespaceId, + plugins: string[], address: ReverseResolutionArgs["address"], chainId: ReverseResolutionArgs["chainId"], - options: Parameters[2], + options: Parameters[4], ): Promise { const { accelerate = true } = options; @@ -68,7 +71,14 @@ export async function resolveReverse( TraceableENSProtocol.ReverseResolution, ReverseResolutionProtocolStep.ResolveReverseName, { name: _reverseName }, - () => resolveForward(_reverseName, REVERSE_RESOLUTION_SELECTION, options), + () => + resolveForward( + namespace, + plugins, + _reverseName, + REVERSE_RESOLUTION_SELECTION, + options, + ), ); // Step 4 — Determine if name record exists @@ -98,7 +108,7 @@ export async function resolveReverse( TraceableENSProtocol.ReverseResolution, ReverseResolutionProtocolStep.ForwardResolveAddressRecord, { name }, - () => resolveForward(name, { addresses: [coinType] }, options), + () => resolveForward(namespace, plugins, name, { addresses: [coinType] }, options), ); const resolvedAddress = addresses[coinType]; diff --git a/apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts b/apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts index 40323fcc6b..111ec8da52 100644 --- a/apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts +++ b/apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts @@ -1,6 +1,8 @@ -import config from "@/config"; - -import { ChainIndexingStatusIds, getENSRootChainId } from "@ensnode/ensnode-sdk"; +import { + ChainIndexingStatusIds, + type EnsIndexerPublicConfig, + getENSRootChainId, +} from "@ensnode/ensnode-sdk"; import type { SubgraphMeta } from "@ensnode/ponder-subgraph"; import type { IndexingStatusMiddlewareVariables } from "@/middleware/indexing-status.middleware"; @@ -18,13 +20,14 @@ import type { IndexingStatusMiddlewareVariables } from "@/middleware/indexing-st */ export function indexingContextToSubgraphMeta( indexingStatus: IndexingStatusMiddlewareVariables["indexingStatus"], + ensIndexerPublicConfig: EnsIndexerPublicConfig, ): SubgraphMeta { // indexing status middleware has never successfully fetched (and cached) an indexing status snapshot // for the lifetime of this service instance. if (indexingStatus instanceof Error) return null; const rootChain = indexingStatus.snapshot.omnichainSnapshot.chains.get( - getENSRootChainId(config.namespace), + getENSRootChainId(ensIndexerPublicConfig.namespace), ); if (!rootChain) return null; @@ -36,7 +39,7 @@ export function indexingContextToSubgraphMeta( case ChainIndexingStatusIds.Backfill: case ChainIndexingStatusIds.Following: { return { - deployment: config.ensIndexerPublicConfig.versionInfo.ensIndexer, + deployment: ensIndexerPublicConfig.versionInfo.ensIndexer, hasIndexingErrors: false, block: { hash: null, diff --git a/apps/ensapi/src/middleware/can-accelerate.middleware.ts b/apps/ensapi/src/middleware/can-accelerate.middleware.ts index b14d91154e..79702d91bf 100644 --- a/apps/ensapi/src/middleware/can-accelerate.middleware.ts +++ b/apps/ensapi/src/middleware/can-accelerate.middleware.ts @@ -1,9 +1,8 @@ -import config from "@/config"; - import { PluginName } from "@ensnode/ensnode-sdk"; import { factory, producing } from "@/lib/hono-factory"; import { makeLogger } from "@/lib/logger"; +import { ensureEnsNodeStackInfoAvailable } from "@/middleware/stack-info.middleware"; const logger = makeLogger("can-accelerate.middleware"); @@ -26,16 +25,19 @@ let prevCanAccelerate = false; export const canAccelerateMiddleware = producing( ["canAccelerate"], factory.createMiddleware(async (c, next) => { + ensureEnsNodeStackInfoAvailable(c); // context must be set by the required middleware if (c.var.isRealtime === undefined) { throw new Error(`Invariant(canAccelerateMiddleware): isRealtime middleware required`); } + const ensIndexerPublicConfig = c.var.stackInfo.ensIndexer; + //////////////////////////// /// Temporary ENSv2 Bailout //////////////////////////// // TODO: re-enable acceleration for ensv2 once implemented - if (config.ensIndexerPublicConfig.plugins.includes(PluginName.ENSv2)) { + if (ensIndexerPublicConfig.plugins.includes(PluginName.ENSv2)) { if (!didWarnCannotAccelerateENSv2) { logger.warn( `ENSApi is temporarily unable to accelerate Resolution API requests while indexing ENSv2. Protocol Acceleration is DISABLED.`, @@ -52,7 +54,7 @@ export const canAccelerateMiddleware = producing( /// Protocol Acceleration Plugin Availability ////////////////////////////////////////////// - const hasProtocolAccelerationPlugin = config.ensIndexerPublicConfig.plugins.includes( + const hasProtocolAccelerationPlugin = ensIndexerPublicConfig.plugins.includes( PluginName.ProtocolAcceleration, ); diff --git a/apps/ensapi/src/middleware/name-tokens.middleware.ts b/apps/ensapi/src/middleware/name-tokens.middleware.ts index 8f78643e76..b2efce3df6 100644 --- a/apps/ensapi/src/middleware/name-tokens.middleware.ts +++ b/apps/ensapi/src/middleware/name-tokens.middleware.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { NameTokensResponseCodes, NameTokensResponseErrorCodes, @@ -9,6 +7,7 @@ import { import { factory } from "@/lib/hono-factory"; import { makeLogger } from "@/lib/logger"; +import { ensureEnsNodeStackInfoAvailable } from "@/middleware/stack-info.middleware"; const logger = makeLogger("name-tokens.middleware"); @@ -31,12 +30,15 @@ const logger = makeLogger("name-tokens.middleware"); */ export const nameTokensApiMiddleware = factory.createMiddleware( async function nameTokensApiMiddleware(c, next) { + ensureEnsNodeStackInfoAvailable(c); // context must be set by the required middleware if (c.var.indexingStatus === undefined) { throw new Error(`Invariant(name-tokens.middleware): indexingStatusMiddleware required`); } - if (!nameTokensPrerequisites.hasEnsIndexerConfigSupport(config.ensIndexerPublicConfig)) { + const ensIndexerPublicConfig = c.var.stackInfo.ensIndexer; + + if (!nameTokensPrerequisites.hasEnsIndexerConfigSupport(ensIndexerPublicConfig)) { return c.json( serializeNameTokensResponse({ responseCode: NameTokensResponseCodes.Error, diff --git a/apps/ensapi/src/middleware/registrar-actions.middleware.ts b/apps/ensapi/src/middleware/registrar-actions.middleware.ts index fc43df3d39..3944b0be3b 100644 --- a/apps/ensapi/src/middleware/registrar-actions.middleware.ts +++ b/apps/ensapi/src/middleware/registrar-actions.middleware.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { hasRegistrarActionsConfigSupport, hasRegistrarActionsIndexingStatusSupport, @@ -9,6 +7,7 @@ import { import { factory } from "@/lib/hono-factory"; import { makeLogger } from "@/lib/logger"; +import { ensureEnsNodeStackInfoAvailable } from "@/middleware/stack-info.middleware"; const logger = makeLogger("registrar-actions.middleware"); @@ -30,12 +29,14 @@ const logger = makeLogger("registrar-actions.middleware"); */ export const registrarActionsApiMiddleware = factory.createMiddleware( async function registrarActionsApiMiddleware(c, next) { + ensureEnsNodeStackInfoAvailable(c); // context must be set by the required middleware if (c.var.indexingStatus === undefined) { throw new Error(`Invariant(registrar-actions.middleware): indexingStatusMiddleware required`); } - const configSupport = hasRegistrarActionsConfigSupport(config.ensIndexerPublicConfig); + const ensIndexerPublicConfig = c.var.stackInfo.ensIndexer; + const configSupport = hasRegistrarActionsConfigSupport(ensIndexerPublicConfig); if (!configSupport.supported) { return c.json( serializeRegistrarActionsResponse({ diff --git a/apps/ensapi/src/middleware/stack-info.middleware.ts b/apps/ensapi/src/middleware/stack-info.middleware.ts index 7011ff6386..1697667460 100644 --- a/apps/ensapi/src/middleware/stack-info.middleware.ts +++ b/apps/ensapi/src/middleware/stack-info.middleware.ts @@ -1,3 +1,5 @@ +import type { Context } from "hono"; + import type { EnsNodeStackInfo } from "@ensnode/ensnode-sdk"; import { stackInfoCache } from "@/cache/stack-info.cache"; @@ -52,3 +54,20 @@ export const stackInfoMiddleware = producing( await next(); }), ); + +/** + * Ensures that the ENSNode Stack Info is available in the Hono context. + * If the Stack Info is not available, an error is thrown. + * + * This is a type guard that narrows the type of `c.var.stackInfo` to {@link EnsNodeStackInfo} in all downstream middlewares and handlers. + * We leverage the fact that {@link stackInfoMiddleware} is the first middleware for all ENSApi routes. + * + * @param c The Hono context + */ +export function ensureEnsNodeStackInfoAvailable( + c: T, +): asserts c is T & { var: T["var"] & { stackInfo: EnsNodeStackInfo } } { + if (c.var.stackInfo instanceof Error) { + throw new Error(`ENSNode Stack Info is not available: ${c.var.stackInfo.message}`); + } +} diff --git a/apps/ensapi/src/middleware/subgraph-meta.middleware.ts b/apps/ensapi/src/middleware/subgraph-meta.middleware.ts index 0d60d4864c..016a9fe2d8 100644 --- a/apps/ensapi/src/middleware/subgraph-meta.middleware.ts +++ b/apps/ensapi/src/middleware/subgraph-meta.middleware.ts @@ -4,6 +4,7 @@ import type { SubgraphMetaVariables } from "@ensnode/ponder-subgraph"; import { indexingContextToSubgraphMeta } from "@/lib/subgraph/indexing-status-to-subgraph-meta"; import type { IndexingStatusMiddlewareVariables } from "@/middleware/indexing-status.middleware"; +import { ensureEnsNodeStackInfoAvailable } from "@/middleware/stack-info.middleware"; /** * Middleware that converts indexing status to subgraph metadata format. @@ -15,11 +16,12 @@ import type { IndexingStatusMiddlewareVariables } from "@/middleware/indexing-st export const subgraphMetaMiddleware = createMiddleware<{ Variables: IndexingStatusMiddlewareVariables & SubgraphMetaVariables; }>(async (c, next) => { + ensureEnsNodeStackInfoAvailable(c); // context must be set by the required middleware if (c.var.indexingStatus === undefined) { throw new Error(`Invariant(subgraphMetaMiddleware): indexingStatusMiddleware required`); } - c.set("_meta", indexingContextToSubgraphMeta(c.var.indexingStatus)); + c.set("_meta", indexingContextToSubgraphMeta(c.var.indexingStatus, c.var.stackInfo.ensIndexer)); await next(); }); diff --git a/apps/ensapi/src/middleware/thegraph-fallback.middleware.ts b/apps/ensapi/src/middleware/thegraph-fallback.middleware.ts index 0cac9478a5..c2062d9763 100644 --- a/apps/ensapi/src/middleware/thegraph-fallback.middleware.ts +++ b/apps/ensapi/src/middleware/thegraph-fallback.middleware.ts @@ -6,6 +6,7 @@ import { canFallbackToTheGraph } from "@ensnode/ensnode-sdk/internal"; import { factory } from "@/lib/hono-factory"; import { makeLogger } from "@/lib/logger"; +import { ensureEnsNodeStackInfoAvailable } from "@/middleware/stack-info.middleware"; const logger = makeLogger("thegraph-fallback.middleware"); @@ -18,6 +19,7 @@ let prevShouldFallback = false; * Middleware that proxies Subgraph requests to The Graph if possible & necessary. */ export const thegraphFallbackMiddleware = factory.createMiddleware(async (c, next) => { + ensureEnsNodeStackInfoAvailable(c); const isRealtime = c.var.isRealtime; // context must be set by the required middleware @@ -25,10 +27,12 @@ export const thegraphFallbackMiddleware = factory.createMiddleware(async (c, nex throw new Error(`Invariant(thegraphFallbackMiddleware): isRealtimeMiddleware expected`); } + const { namespace, isSubgraphCompatible } = c.var.stackInfo.ensIndexer; + const fallback = canFallbackToTheGraph({ - namespace: config.namespace, + namespace, + isSubgraphCompatible, theGraphApiKey: config.theGraphApiKey, - isSubgraphCompatible: config.ensIndexerPublicConfig.isSubgraphCompatible, }); // log one warning to the console if !canFallback @@ -46,7 +50,7 @@ export const thegraphFallbackMiddleware = factory.createMiddleware(async (c, nex } case "no-subgraph-url": { logger.warn( - `ENSApi can NOT fallback to The Graph: the connected ENSIndexer's namespace ('${config.namespace}') is not supported by The Graph.`, + `ENSApi can NOT fallback to The Graph: the connected ENSIndexer's namespace ('${namespace}') is not supported by The Graph.`, ); break; } diff --git a/apps/ensapi/src/omnigraph-api/builder.ts b/apps/ensapi/src/omnigraph-api/builder.ts index eafbae8696..4b161ce914 100644 --- a/apps/ensapi/src/omnigraph-api/builder.ts +++ b/apps/ensapi/src/omnigraph-api/builder.ts @@ -25,7 +25,7 @@ import type { import { getNamedType } from "graphql"; import superjson from "superjson"; -import type { context } from "@/omnigraph-api/context"; +import type { createYogaContextForNamespace } from "@/omnigraph-api/context"; const tracer = trace.getTracer("graphql"); const createSpan = createOpenTelemetryWrapper(tracer, { @@ -77,7 +77,7 @@ export type BuilderScalars = { }; export const builder = new SchemaBuilder<{ - Context: ReturnType; + Context: ReturnType; Scalars: BuilderScalars; // the following ensures via typechecker that every t.connection returns a totalCount field diff --git a/apps/ensapi/src/omnigraph-api/context.ts b/apps/ensapi/src/omnigraph-api/context.ts index a1ae5ac114..165b542685 100644 --- a/apps/ensapi/src/omnigraph-api/context.ts +++ b/apps/ensapi/src/omnigraph-api/context.ts @@ -2,6 +2,8 @@ import DataLoader from "dataloader"; import { getUnixTime } from "date-fns"; import type { CanonicalPath, ENSv1DomainId, ENSv2DomainId } from "enssdk"; +import type { ENSNamespaceId } from "@ensnode/datasources"; + import { getV1CanonicalPath, getV2CanonicalPath } from "./lib/get-canonical-path"; /** @@ -15,20 +17,21 @@ const createV1CanonicalPathLoader = () => Promise.all(domainIds.map((id) => getV1CanonicalPath(id).catch(errorAsValue))), ); -const createV2CanonicalPathLoader = () => +const createV2CanonicalPathLoader = (namespace: ENSNamespaceId) => new DataLoader(async (domainIds) => - Promise.all(domainIds.map((id) => getV2CanonicalPath(id).catch(errorAsValue))), + Promise.all(domainIds.map((id) => getV2CanonicalPath(namespace, id).catch(errorAsValue))), ); /** - * Constructs a new GraphQL Context per-request. + * Constructs a new GraphQL Context per-request for the given {@link ENSNamespaceId}. * * @dev make sure that anything that is per-request (like dataloaders) are newly created in this fn */ -export const context = () => ({ +export const createYogaContextForNamespace = (namespace: ENSNamespaceId) => ({ + namespace, now: BigInt(getUnixTime(new Date())), loaders: { v1CanonicalPath: createV1CanonicalPathLoader(), - v2CanonicalPath: createV2CanonicalPathLoader(), + v2CanonicalPath: createV2CanonicalPathLoader(namespace), }, }); diff --git a/apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts b/apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts index c14e8a0e28..b18dcb8522 100644 --- a/apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts +++ b/apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts @@ -1,11 +1,8 @@ -import config from "@/config"; - import { sql } from "drizzle-orm"; -import { maybeGetENSv2RootRegistryId } from "@ensnode/ensnode-sdk"; +import { type ENSNamespaceId, maybeGetENSv2RootRegistryId } from "@ensnode/ensnode-sdk"; import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; -import { lazy } from "@/lib/lazy"; /** * The maximum depth to traverse the ENSv2 namegraph in order to construct the set of Canonical @@ -22,10 +19,6 @@ import { lazy } from "@/lib/lazy"; */ const CANONICAL_REGISTRIES_MAX_DEPTH = 16; -// lazy() defers construction until first use so that this module can be -// imported without env vars being present (e.g. during OpenAPI generation). -const getENSV2RootRegistryId = lazy(() => maybeGetENSv2RootRegistryId(config.namespace)); - /** * Builds a recursive CTE that traverses from the ENSv2 Root Registry to construct a set of all * Canonical Registries. A Canonical Registry is an ENSv2 Registry that is the Root Registry or the @@ -33,9 +26,10 @@ const getENSV2RootRegistryId = lazy(() => maybeGetENSv2RootRegistryId(config.nam * * TODO: could this be optimized further, perhaps as a materialized view? */ -export const getCanonicalRegistriesCTE = () => { +export const getCanonicalRegistriesCTE = (namespace: ENSNamespaceId) => { + const rootRegistryId = maybeGetENSv2RootRegistryId(namespace); // if ENSv2 is not defined, return an empty set with identical structure to below - if (!getENSV2RootRegistryId()) { + if (!rootRegistryId) { return ensDb .select({ id: sql`registry_id`.as("id") }) .from(sql`(SELECT NULL::text AS registry_id WHERE FALSE) AS canonical_registries_cte`) @@ -53,7 +47,7 @@ export const getCanonicalRegistriesCTE = () => { sql` ( WITH RECURSIVE canonical_registries AS ( - SELECT ${getENSV2RootRegistryId()}::text AS registry_id, 0 AS depth + SELECT ${rootRegistryId}::text AS registry_id, 0 AS depth UNION ALL SELECT rcd.registry_id, cr.depth + 1 FROM ${ensIndexerSchema.registryCanonicalDomain} rcd diff --git a/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts b/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts index 142030da67..93295ac86a 100644 --- a/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts +++ b/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts @@ -5,7 +5,7 @@ import { and, count } from "drizzle-orm"; import { ensDb } from "@/lib/ensdb/singleton"; import { withActiveSpanAsync } from "@/lib/instrumentation/auto-span"; import { makeLogger } from "@/lib/logger"; -import type { context as createContext } from "@/omnigraph-api/context"; +import type { createYogaContextForNamespace } from "@/omnigraph-api/context"; import type { DomainsWithOrderingMetadata, DomainsWithOrderingMetadataResult, @@ -77,7 +77,7 @@ function getOrderValueFromResult( * @param args - The domains CTE, optional ordering, and relay connection args */ export function resolveFindDomains( - context: ReturnType, + context: ReturnType, { domains, order, diff --git a/apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-canonical.ts b/apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-canonical.ts index bc5d63213f..023ac8d081 100644 --- a/apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-canonical.ts +++ b/apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-canonical.ts @@ -1,5 +1,7 @@ import { eq, isNotNull, isNull, or } from "drizzle-orm"; +import type { ENSNamespaceId } from "@ensnode/datasources"; + import { ensDb } from "@/lib/ensdb/singleton"; import { getCanonicalRegistriesCTE } from "../canonical-registries-cte"; @@ -14,8 +16,8 @@ import { type BaseDomainSet, selectBase } from "./base-domain-set"; * Uses LEFT JOIN with canonical registries CTE: v1 domains pass through (registryId IS NULL), * v2 domains must match a canonical registry. */ -export function filterByCanonical(base: BaseDomainSet) { - const canonicalRegistries = getCanonicalRegistriesCTE(); +export function filterByCanonical(namespace: ENSNamespaceId, base: BaseDomainSet) { + const canonicalRegistries = getCanonicalRegistriesCTE(namespace); return ensDb .select(selectBase(base)) diff --git a/apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts b/apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts index 1fc25c60f9..570a919e19 100644 --- a/apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts +++ b/apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { sql } from "drizzle-orm"; import { type CanonicalPath, @@ -10,15 +8,11 @@ import { type RegistryId, } from "enssdk"; -import { maybeGetENSv2RootRegistryId } from "@ensnode/ensnode-sdk"; +import { type ENSNamespaceId, maybeGetENSv2RootRegistryId } from "@ensnode/ensnode-sdk"; import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; -import { lazy } from "@/lib/lazy"; const MAX_DEPTH = 16; -// lazy() defers construction until first use so that this module can be -// imported without env vars being present (e.g. during OpenAPI generation). -const getENSv2RootRegistryId = lazy(() => maybeGetENSv2RootRegistryId(config.namespace)); /** * Provide the canonical parents for an ENSv1 Domain. @@ -75,8 +69,11 @@ export async function getV1CanonicalPath(domainId: ENSv1DomainId): Promise { - const rootRegistryId = getENSv2RootRegistryId(); +export async function getV2CanonicalPath( + namespace: ENSNamespaceId, + domainId: ENSv2DomainId, +): Promise { + const rootRegistryId = maybeGetENSv2RootRegistryId(namespace); // if the ENSv2 Root Registry is not defined, null if (!rootRegistryId) return null; diff --git a/apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts b/apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts index e1ff67ff88..6208e28bc0 100644 --- a/apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts +++ b/apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { trace } from "@opentelemetry/api"; import { Param, sql } from "drizzle-orm"; import { @@ -14,17 +12,12 @@ import { type RegistryId, } from "enssdk"; -import { maybeGetENSv2RootRegistryId } from "@ensnode/ensnode-sdk"; +import { type ENSNamespaceId, maybeGetENSv2RootRegistryId } from "@ensnode/ensnode-sdk"; import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; import { withActiveSpanAsync } from "@/lib/instrumentation/auto-span"; -import { lazy } from "@/lib/lazy"; import { makeLogger } from "@/lib/logger"; -// lazy() defers construction until first use so that this module can be -// imported without env vars being present (e.g. during OpenAPI generation). -const _maybeGetENSv2RootRegistryId = lazy(() => maybeGetENSv2RootRegistryId(config.namespace)); - const tracer = trace.getTracer("get-domain-by-interpreted-name"); const logger = makeLogger("get-domain-by-interpreted-name"); const v1Logger = makeLogger("get-domain-by-interpreted-name:v1"); @@ -64,11 +57,12 @@ const v2Logger = makeLogger("get-domain-by-interpreted-name:v2"); * not the alias by which it was queried ('sub.alias.eth'). */ export async function getDomainIdByInterpretedName( + namespace: ENSNamespaceId, name: InterpretedName, ): Promise { return withActiveSpanAsync(tracer, "getDomainIdByInterpretedName", { name }, async () => { // Domains addressable in v2 are preferred, but v1 lookups are cheap, so just do them both ahead of time - const rootRegistryId = _maybeGetENSv2RootRegistryId(); + const rootRegistryId = maybeGetENSv2RootRegistryId(namespace); const [v1DomainId, v2DomainId] = await Promise.all([ withActiveSpanAsync(tracer, "v1_getDomainId", {}, () => diff --git a/apps/ensapi/src/omnigraph-api/schema/account.ts b/apps/ensapi/src/omnigraph-api/schema/account.ts index 55a4d07365..2b7bab0b73 100644 --- a/apps/ensapi/src/omnigraph-api/schema/account.ts +++ b/apps/ensapi/src/omnigraph-api/schema/account.ts @@ -80,7 +80,8 @@ AccountRef.implement({ const base = domainsBase(); const owned = filterByOwner(base, parent.id); const named = filterByName(owned, where?.name); - const canonical = where?.canonical === true ? filterByCanonical(named) : named; + const canonical = + where?.canonical === true ? filterByCanonical(context.namespace, named) : named; const domains = withOrderingMetadata(canonical); return resolveFindDomains(context, { domains, order, ...connectionArgs }); }, diff --git a/apps/ensapi/src/omnigraph-api/schema/query.ts b/apps/ensapi/src/omnigraph-api/schema/query.ts index 58148775ec..894b7aab45 100644 --- a/apps/ensapi/src/omnigraph-api/schema/query.ts +++ b/apps/ensapi/src/omnigraph-api/schema/query.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; import { makePermissionsId, makeRegistryId, makeResolverId } from "enssdk"; @@ -144,7 +142,8 @@ builder.queryType({ resolve: (_, { where, order, ...connectionArgs }, context) => { const base = domainsBase(); const named = filterByName(base, where.name); - const canonical = where.canonical === true ? filterByCanonical(named) : named; + const canonical = + where.canonical === true ? filterByCanonical(context.namespace, named) : named; const domains = withOrderingMetadata(canonical); return resolveFindDomains(context, { domains, order, ...connectionArgs }); @@ -159,9 +158,9 @@ builder.queryType({ type: DomainInterfaceRef, args: { by: t.arg({ type: DomainIdInput, required: true }) }, nullable: true, - resolve: (parent, args, ctx, info) => { + resolve: (parent, args, context, info) => { if (args.by.id !== undefined) return args.by.id; - return getDomainIdByInterpretedName(args.by.name); + return getDomainIdByInterpretedName(context.namespace, args.by.name); }, }), @@ -222,7 +221,7 @@ builder.queryType({ type: RegistryRef, // TODO: make this nullable: false after all namespaces define ENSv2Root nullable: true, - resolve: () => maybeGetENSv2RootRegistryId(config.namespace), + resolve: (parent, args, context, info) => maybeGetENSv2RootRegistryId(context.namespace), }), }), }); diff --git a/apps/ensapi/src/omnigraph-api/schema/resolver.ts b/apps/ensapi/src/omnigraph-api/schema/resolver.ts index c2dd4179f9..9f91f31bd6 100644 --- a/apps/ensapi/src/omnigraph-api/schema/resolver.ts +++ b/apps/ensapi/src/omnigraph-api/schema/resolver.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay"; import { and, eq } from "drizzle-orm"; import { @@ -125,7 +123,7 @@ ResolverRef.implement({ description: "Whether Resolver is a BridgedResolver.", type: AccountIdRef, nullable: true, - resolve: (parent) => isBridgedResolver(config.namespace, parent), + resolve: (parent, args, context) => isBridgedResolver(context.namespace, parent), }), //////////////////////// diff --git a/apps/ensapi/src/omnigraph-api/yoga.ts b/apps/ensapi/src/omnigraph-api/yoga.ts index 528e80546d..718a7742e9 100644 --- a/apps/ensapi/src/omnigraph-api/yoga.ts +++ b/apps/ensapi/src/omnigraph-api/yoga.ts @@ -4,20 +4,24 @@ import { createYoga } from "graphql-yoga"; +import type { ENSNamespaceId } from "@ensnode/datasources"; + import { makeLogger } from "@/lib/logger"; -import { context } from "@/omnigraph-api/context"; +import { createYogaContextForNamespace } from "@/omnigraph-api/context"; import { schema } from "@/omnigraph-api/schema"; const logger = makeLogger("omnigraph"); -export const yoga = createYoga({ - graphqlEndpoint: "*", - schema, - context, - // CORS is handled by the Hono middleware in app.ts - cors: false, - graphiql: { - defaultQuery: `query DomainsByOwner { +export const createYogaForNamespace = (namespace: ENSNamespaceId) => { + const context = createYogaContextForNamespace(namespace); + const yoga = createYoga({ + graphqlEndpoint: "*", + schema, + context, + // CORS is handled by the Hono middleware in app.ts + cors: false, + graphiql: { + defaultQuery: `query DomainsByOwner { account(by: { address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }) { domains { edges { @@ -37,15 +41,17 @@ export const yoga = createYoga({ } } }`, - }, + }, - // integrate logging with pino - logging: logger, + // integrate logging with pino + logging: logger, - plugins: [ - // TODO: plugins - // maxTokensPlugin({ n: maxOperationTokens }), - // maxDepthPlugin({ n: maxOperationDepth, ignoreIntrospection: false }), - // maxAliasesPlugin({ n: maxOperationAliases, allowList: [] }), - ], -}); + plugins: [ + // TODO: plugins + // maxTokensPlugin({ n: maxOperationTokens }), + // maxDepthPlugin({ n: maxOperationDepth, ignoreIntrospection: false }), + // maxAliasesPlugin({ n: maxOperationAliases, allowList: [] }), + ], + }); + return yoga; +}; diff --git a/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts b/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts index 7f4bf6afa1..251a8184a3 100644 --- a/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts +++ b/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts @@ -1,8 +1,5 @@ import { describe, expect, it } from "vitest"; -import { ENSNamespaceIds } from "@ensnode/datasources"; - -import { PluginName } from "../../ensindexer/config/types"; import { deserializeEnsApiPublicConfig } from "./deserialize"; import { serializeEnsApiPublicConfig } from "./serialize"; import type { SerializedEnsApiPublicConfig } from "./serialized-types"; @@ -17,25 +14,6 @@ const MOCK_ENSAPI_PUBLIC_CONFIG = { canFallback: false, reason: "no-api-key", }, - ensIndexerPublicConfig: { - namespace: ENSNamespaceIds.Mainnet, - ensIndexerSchemaName: "ensindexer_0", - ensRainbowPublicConfig: { - version: "0.36.0", - labelSet: { labelSetId: "subgraph", highestLabelSetVersion: 0 }, - recordsCount: 100, - }, - indexedChainIds: new Set([1]), - isSubgraphCompatible: false, - labelSet: { labelSetId: "subgraph", labelSetVersion: 0 }, - plugins: [PluginName.Subgraph], - versionInfo: { - ensDb: "0.36.0", - ensIndexer: "0.36.0", - ensNormalize: "1.1.1", - ponder: "0.5.0", - }, - }, } satisfies EnsApiPublicConfig; const MOCK_SERIALIZED_ENSAPI_PUBLIC_CONFIG = serializeEnsApiPublicConfig(MOCK_ENSAPI_PUBLIC_CONFIG); @@ -54,25 +32,6 @@ describe("ENSApi Config Serialization/Deserialization", () => { canFallback: false, reason: "no-api-key", }, - ensIndexerPublicConfig: { - namespace: ENSNamespaceIds.Mainnet, - ensIndexerSchemaName: "ensindexer_0", - ensRainbowPublicConfig: { - version: "0.36.0", - labelSet: { labelSetId: "subgraph", highestLabelSetVersion: 0 }, - recordsCount: 100, - }, - indexedChainIds: [1], - isSubgraphCompatible: false, - labelSet: { labelSetId: "subgraph", labelSetVersion: 0 }, - plugins: [PluginName.Subgraph], - versionInfo: { - ensDb: "0.36.0", - ensIndexer: "0.36.0", - ensNormalize: "1.1.1", - ponder: "0.5.0", - }, - }, } satisfies SerializedEnsApiPublicConfig); }); }); diff --git a/packages/ensnode-sdk/src/ensapi/config/deserialize.ts b/packages/ensnode-sdk/src/ensapi/config/deserialize.ts index 5c22c21809..c6a220fcbf 100644 --- a/packages/ensnode-sdk/src/ensapi/config/deserialize.ts +++ b/packages/ensnode-sdk/src/ensapi/config/deserialize.ts @@ -1,6 +1,5 @@ import { prettifyError } from "zod/v4"; -import { buildUnvalidatedEnsIndexerPublicConfig } from "../../ensindexer/config/deserialize"; import type { Unvalidated } from "../../shared/types"; import type { SerializedEnsApiPublicConfig } from "./serialized-types"; import type { EnsApiPublicConfig } from "./types"; @@ -19,12 +18,7 @@ import { export function buildUnvalidatedEnsApiPublicConfig( serializedPublicConfig: SerializedEnsApiPublicConfig, ): Unvalidated { - return { - ...serializedPublicConfig, - ensIndexerPublicConfig: buildUnvalidatedEnsIndexerPublicConfig( - serializedPublicConfig.ensIndexerPublicConfig, - ), - }; + return serializedPublicConfig; } /** diff --git a/packages/ensnode-sdk/src/ensapi/config/serialize.ts b/packages/ensnode-sdk/src/ensapi/config/serialize.ts index 53e1303d7c..5824028d84 100644 --- a/packages/ensnode-sdk/src/ensapi/config/serialize.ts +++ b/packages/ensnode-sdk/src/ensapi/config/serialize.ts @@ -1,4 +1,3 @@ -import { serializeEnsIndexerPublicConfig } from "../../ensindexer/config/serialize"; import type { SerializedEnsApiPublicConfig } from "./serialized-types"; import type { EnsApiPublicConfig } from "./types"; @@ -8,10 +7,9 @@ import type { EnsApiPublicConfig } from "./types"; export function serializeEnsApiPublicConfig( config: EnsApiPublicConfig, ): SerializedEnsApiPublicConfig { - const { ensIndexerPublicConfig, theGraphFallback, versionInfo } = config; + const { theGraphFallback, versionInfo } = config; return { - ensIndexerPublicConfig: serializeEnsIndexerPublicConfig(ensIndexerPublicConfig), theGraphFallback, versionInfo, } satisfies SerializedEnsApiPublicConfig; diff --git a/packages/ensnode-sdk/src/ensapi/config/serialized-types.ts b/packages/ensnode-sdk/src/ensapi/config/serialized-types.ts index 6a88e8fe67..c44aeef284 100644 --- a/packages/ensnode-sdk/src/ensapi/config/serialized-types.ts +++ b/packages/ensnode-sdk/src/ensapi/config/serialized-types.ts @@ -1,16 +1,9 @@ -import type { SerializedEnsIndexerPublicConfig } from "../../ensindexer/config/serialized-types"; import type { EnsApiPublicConfig } from "./types"; /** * Serialized representation of {@link EnsApiPublicConfig} */ -export interface SerializedEnsApiPublicConfig - extends Omit { - /** - * Serialized representation of {@link EnsApiPublicConfig.ensIndexerPublicConfig}. - */ - ensIndexerPublicConfig: SerializedEnsIndexerPublicConfig; -} +export type SerializedEnsApiPublicConfig = EnsApiPublicConfig; /** * Serialized representation of {@link EnsApiPublicConfig} diff --git a/packages/ensnode-sdk/src/ensapi/config/types.ts b/packages/ensnode-sdk/src/ensapi/config/types.ts index 66ae798e4c..bf252b33fe 100644 --- a/packages/ensnode-sdk/src/ensapi/config/types.ts +++ b/packages/ensnode-sdk/src/ensapi/config/types.ts @@ -1,4 +1,3 @@ -import type { EnsIndexerPublicConfig } from "../../ensindexer/config/types"; import type { TheGraphCannotFallbackReason, TheGraphFallback } from "../../shared/config/thegraph"; export type { TheGraphCannotFallbackReason, TheGraphFallback }; @@ -36,14 +35,6 @@ export interface EnsApiPublicConfig { */ theGraphFallback: TheGraphFallback; - /** - * Complete ENSIndexer public configuration - * - * Contains all ENSIndexer public configuration including - * namespace, plugins, version info, etc. - */ - ensIndexerPublicConfig: EnsIndexerPublicConfig; - /** * Version info about ENSApi. */ diff --git a/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts b/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts index 19e5eff2dd..7e105aa80a 100644 --- a/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts @@ -1,9 +1,5 @@ import { z } from "zod/v4"; -import { - makeEnsIndexerPublicConfigSchema, - makeSerializedEnsIndexerPublicConfigSchema, -} from "../../ensindexer/config/zod-schemas"; import { TheGraphCannotFallbackReasonSchema, TheGraphFallbackSchema, @@ -27,7 +23,6 @@ export function makeEnsApiPublicConfigSchema(valueLabel?: string) { return z.object({ theGraphFallback: TheGraphFallbackSchema, - ensIndexerPublicConfig: makeEnsIndexerPublicConfigSchema(`${label}.ensIndexerPublicConfig`), versionInfo: makeEnsApiVersionInfoSchema(`${label}.versionInfo`), }); } @@ -43,9 +38,6 @@ export function makeSerializedEnsApiPublicConfigSchema(valueLabel?: string) { const label = valueLabel ?? "ENSApiPublicConfig"; return z.object({ - ensIndexerPublicConfig: makeSerializedEnsIndexerPublicConfigSchema( - `${label}.ensIndexerPublicConfig`, - ), theGraphFallback: TheGraphFallbackSchema, versionInfo: makeEnsApiVersionInfoSchema(`${label}.versionInfo`), }); diff --git a/packages/ensnode-sdk/src/ensnode/client.test.ts b/packages/ensnode-sdk/src/ensnode/client.test.ts index d23e7a9ce0..f2c9a0fb0c 100644 --- a/packages/ensnode-sdk/src/ensnode/client.test.ts +++ b/packages/ensnode-sdk/src/ensnode/client.test.ts @@ -69,35 +69,6 @@ const EXAMPLE_ENSAPI_CONFIG_RESPONSE = { canFallback: false, reason: "no-api-key", }, - ensIndexerPublicConfig: { - ensRainbowPublicConfig: { - version: "0.31.0", - labelSet: { labelSetId: "subgraph", highestLabelSetVersion: 0 }, - recordsCount: 100, - }, - labelSet: { - labelSetId: "subgraph", - labelSetVersion: 0, - }, - indexedChainIds: [1, 8453, 59144, 10, 42161, 534352], - ensIndexerSchemaName: "alphaSchema0.31.0", - isSubgraphCompatible: false, - namespace: "mainnet", - plugins: [ - PluginName.Subgraph, - PluginName.Basenames, - PluginName.Lineanames, - PluginName.ThreeDNS, - PluginName.ProtocolAcceleration, - PluginName.Registrars, - ], - versionInfo: { - ponder: "0.11.43", - ensDb: "0.32.0", - ensIndexer: "0.32.0", - ensNormalize: "1.11.1", - }, - }, } satisfies SerializedEnsApiPublicConfig; const EXAMPLE_ENSDB_PUBLIC_RESPONSE = { diff --git a/packages/ensnode-sdk/src/stack-info/ensnode-stack-info.ts b/packages/ensnode-sdk/src/stack-info/ensnode-stack-info.ts index 94f39d1955..117f8f36f4 100644 --- a/packages/ensnode-sdk/src/stack-info/ensnode-stack-info.ts +++ b/packages/ensnode-sdk/src/stack-info/ensnode-stack-info.ts @@ -38,11 +38,13 @@ export interface EnsNodeStackInfo { export function buildEnsNodeStackInfo( ensApiPublicConfig: EnsApiPublicConfig, ensDbPublicConfig: EnsDbPublicConfig, + ensIndexerPublicConfig: EnsIndexerPublicConfig, + ensRainbowPublicConfig?: EnsRainbowPublicConfig, ): EnsNodeStackInfo { return { ensApi: ensApiPublicConfig, ensDb: ensDbPublicConfig, - ensIndexer: ensApiPublicConfig.ensIndexerPublicConfig, - ensRainbow: ensApiPublicConfig.ensIndexerPublicConfig.ensRainbowPublicConfig, + ensIndexer: ensIndexerPublicConfig, + ensRainbow: ensRainbowPublicConfig, }; } diff --git a/packages/ensnode-sdk/src/stack-info/serialize/ensnode-stack-info.ts b/packages/ensnode-sdk/src/stack-info/serialize/ensnode-stack-info.ts index 1c1f2f2638..cccc4ba814 100644 --- a/packages/ensnode-sdk/src/stack-info/serialize/ensnode-stack-info.ts +++ b/packages/ensnode-sdk/src/stack-info/serialize/ensnode-stack-info.ts @@ -1,4 +1,3 @@ -import { serializeEnsApiPublicConfig } from "../../ensapi/config/serialize"; import type { SerializedEnsApiPublicConfig } from "../../ensapi/config/serialized-types"; import type { SerializedEnsDbPublicConfig } from "../../ensdb/serialize/config"; import { serializeEnsIndexerPublicConfig } from "../../ensindexer/config/serialize"; @@ -21,7 +20,7 @@ export interface SerializedEnsNodeStackInfo { */ export function serializeEnsNodeStackInfo(stackInfo: EnsNodeStackInfo): SerializedEnsNodeStackInfo { return { - ensApi: serializeEnsApiPublicConfig(stackInfo.ensApi), + ensApi: stackInfo.ensApi, ensDb: stackInfo.ensDb, ensIndexer: serializeEnsIndexerPublicConfig(stackInfo.ensIndexer), ensRainbow: stackInfo.ensRainbow,