Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1f1e3df
Update ENSApi Indexing Status API data model
tk-o Feb 24, 2026
e518a43
Update HTTP handlers for ENSApi
tk-o Feb 24, 2026
ac26750
Update Indexing Status cache for ENSApi
tk-o Feb 24, 2026
55414e5
Rename `ENSNodeProvider*` types
tk-o Feb 24, 2026
2557708
Rename `useENSNodeSDKConfig` to `useEnsApiProviderOptions`
tk-o Feb 24, 2026
da220f3
Remove `useENSNodeConfig` hook
tk-o Feb 24, 2026
098ff6e
Update `useResolvedIdentity` to require `namespace` param
tk-o Feb 24, 2026
f7603b8
Update ENSNode hooks to apply renamed functions and types
tk-o Feb 24, 2026
1d45940
Update `useIndexingStatusWithSwr` hook to also cache `config: EnsApiP…
tk-o Feb 24, 2026
ba81822
Create `useEnsApiConfig` for conveniet use of `EnsApiPublicConfig` value
tk-o Feb 24, 2026
72d7ae7
Replace applications of removed `useENSNodeConfig` hook with `useEnsA…
tk-o Feb 24, 2026
c3a1579
Reduce use of `useENSNodeConfig` (removed) and `useIndexingStatusWit…
tk-o Feb 24, 2026
352be15
Replace applications of removed `SelectedENSNodeProvider` with `Selec…
tk-o Feb 24, 2026
f88cc15
Merge remote-tracking branch 'origin/main' into ensapi-merge-config-w…
tk-o Mar 2, 2026
709443d
Fix references to updated ENSApi response data model
tk-o Mar 2, 2026
b768b3d
Fix references to updated ENSIndexer response data model
tk-o Mar 2, 2026
2a2b40f
Fix references to updated Indexing Status data model in ENSAdmin
tk-o Mar 2, 2026
5edb862
Remove config route definition from ENSApi routes
tk-o Mar 2, 2026
073f642
Apply AI PR feedback
tk-o Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions apps/ensadmin/src/app/mock/config-api.mock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { deserializeENSIndexerPublicConfig } from "@ensnode/ensnode-sdk";
import {
deserializeENSIndexerPublicConfig,
SerializedEnsApiConfigResponse,
SerializedEnsIndexerConfigResponse,
} from "@ensnode/ensnode-sdk";

export const ensIndexerPublicConfig = deserializeENSIndexerPublicConfig({
const serializedEnsIndexerPublicConfig = {
labelSet: {
labelSetId: "subgraph",
labelSetVersion: 0,
Expand All @@ -27,4 +31,17 @@ export const ensIndexerPublicConfig = deserializeENSIndexerPublicConfig({
ensRainbowSchema: 3,
ensNormalize: "1.11.1",
},
});
} satisfies SerializedEnsIndexerConfigResponse;

export const ensIndexerPublicConfig = deserializeENSIndexerPublicConfig(
serializedEnsIndexerPublicConfig,
);

export const serializedEnsApiPublicConfig = {
ensIndexerPublicConfig: serializedEnsIndexerPublicConfig,
theGraphFallback: {
canFallback: true,
url: "https://api.thegraph.com/subgraphs/name/ensdomains/ens",
},
version: "0.35.0",
} satisfies SerializedEnsApiConfigResponse;
15 changes: 10 additions & 5 deletions apps/ensadmin/src/app/mock/indexing-stats/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useEffect, useState } from "react";
import {
CrossChainIndexingStatusSnapshot,
createRealtimeIndexingStatusProjection,
EnsApiIndexingStatusResponseOk,
IndexingStatusResponseCodes,
IndexingStatusResponseOk,
OmnichainIndexingStatusIds,
Expand Down Expand Up @@ -37,7 +38,7 @@ let loadingTimeoutId: number;

async function fetchMockedIndexingStatus(
selectedVariant: Variant,
): Promise<CrossChainIndexingStatusSnapshot> {
): Promise<EnsApiIndexingStatusResponseOk> {
// always try clearing loading timeout when performing a mocked fetch
// this way we get a fresh and very long request to observe the loading state
if (loadingTimeoutId) {
Expand All @@ -53,14 +54,14 @@ async function fetchMockedIndexingStatus(
selectedVariant
] as IndexingStatusResponseOk;

return response.realtimeProjection.snapshot;
return response;
}
case "Error ResponseCode":
throw new Error(
"Received Indexing Status response with responseCode other than 'ok' which will not be cached.",
);
case "Loading":
return new Promise<CrossChainIndexingStatusSnapshot>((_resolve, reject) => {
return new Promise<EnsApiIndexingStatusResponseOk>((_resolve, reject) => {
loadingTimeoutId = +setTimeout(reject, 5 * 60 * 1_000);
});
case "Loading Error":
Expand All @@ -77,10 +78,14 @@ export default function MockIndexingStatusPage() {
const mockedIndexingStatus = useQuery({
queryKey: ["mock", "useIndexingStatus", selectedVariant],
queryFn: () => fetchMockedIndexingStatus(selectedVariant),
select: (cachedSnapshot) => {
select: (response) => {
return {
responseCode: IndexingStatusResponseCodes.Ok,
realtimeProjection: createRealtimeIndexingStatusProjection(cachedSnapshot, now),
realtimeProjection: createRealtimeIndexingStatusProjection(
response.realtimeProjection.snapshot,
now,
),
config: response.config,
} satisfies IndexingStatusResponseOk;
},
retry: false, // allows loading error to be observed immediately
Expand Down
6 changes: 6 additions & 0 deletions apps/ensadmin/src/app/mock/indexing-status-api.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
type SerializedOmnichainIndexingStatusSnapshotUnstarted,
} from "@ensnode/ensnode-sdk";

import { serializedEnsApiPublicConfig } from "@/app/mock/config-api.mock";

export const indexingStatusResponseError: IndexingStatusResponseError = {
responseCode: IndexingStatusResponseCodes.Error,
};
Expand Down Expand Up @@ -85,6 +87,7 @@ export const indexingStatusResponseOkOmnichain: Record<
} satisfies SerializedOmnichainIndexingStatusSnapshotUnstarted,
},
},
config: serializedEnsApiPublicConfig,
}),

[OmnichainIndexingStatusIds.Backfill]: deserializeIndexingStatusResponse({
Expand Down Expand Up @@ -163,6 +166,7 @@ export const indexingStatusResponseOkOmnichain: Record<
} satisfies SerializedOmnichainIndexingStatusSnapshotBackfill,
},
},
config: serializedEnsApiPublicConfig,
}),

[OmnichainIndexingStatusIds.Following]: deserializeIndexingStatusResponse({
Expand Down Expand Up @@ -256,6 +260,7 @@ export const indexingStatusResponseOkOmnichain: Record<
} satisfies SerializedOmnichainIndexingStatusSnapshotFollowing,
},
},
config: serializedEnsApiPublicConfig,
}),

[OmnichainIndexingStatusIds.Completed]: deserializeIndexingStatusResponse({
Expand Down Expand Up @@ -293,5 +298,6 @@ export const indexingStatusResponseOkOmnichain: Record<
} satisfies SerializedOmnichainIndexingStatusSnapshotCompleted,
},
},
config: serializedEnsApiPublicConfig,
}),
};
16 changes: 16 additions & 0 deletions apps/ensadmin/src/components/config/useEnsApiConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useQuery } from "@tanstack/react-query";

import { useEnsApiProviderOptions } from "@ensnode/ensnode-react";

import { useIndexingStatusWithSwr } from "@/components/indexing-status";

export function useEnsApiConfig() {
const ensApiProviderOptions = useEnsApiProviderOptions();
const indexingStatus = useIndexingStatusWithSwr();

return useQuery({
enabled: indexingStatus.isSuccess,
queryKey: ["swr", ensApiProviderOptions.client.url.href, "config"],
queryFn: async () => indexingStatus.data?.config, // enabled flag ensures this is only called when indexingStatus.data is available
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

queryFn can currently return undefined because of the optional chain (indexingStatus.data?.config). Since enabled gates on indexingStatus.isSuccess, you can (and should) return indexingStatus.data.config directly (or throw if unexpectedly missing) to keep the query typed as always returning a config object.

Suggested change
queryFn: async () => indexingStatus.data?.config, // enabled flag ensures this is only called when indexingStatus.data is available
queryFn: async () => {
if (!indexingStatus.data || !indexingStatus.data.config) {
throw new Error("Indexing status config is unavailable despite isSuccess being true.");
}
return indexingStatus.data.config;
},

Copilot uses AI. Check for mistakes.
});
}
42 changes: 22 additions & 20 deletions apps/ensadmin/src/components/connection/cards/ensnode-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { ChainIcon, getChainName } from "@namehash/namehash-ui";
import { History, Replace } from "lucide-react";
import { Fragment, ReactNode } from "react";

import { useENSNodeConfig } from "@ensnode/ensnode-react";
import { type ENSApiPublicConfig, getENSRootChainId } from "@ensnode/ensnode-sdk";
import { type EnsApiPublicConfig, getENSRootChainId } from "@ensnode/ensnode-sdk";

import { useEnsApiConfig } from "@/components/config/useEnsApiConfig";
import { ErrorInfo, type ErrorInfoProps } from "@/components/error-info";
import { ENSApiIcon } from "@/components/icons/ensnode-apps/ensapi-icon";
import { ENSDbIcon } from "@/components/icons/ensnode-apps/ensdb-icon";
Expand Down Expand Up @@ -96,7 +96,7 @@ function ENSNodeCardLoadingSkeleton() {
* Props for ENSNodeConfigCardDisplay - display component that accepts props for testing/mocking
*/
export interface ENSNodeConfigCardDisplayProps {
ensApiPublicConfig: ENSApiPublicConfig;
ensApiPublicConfig: EnsApiPublicConfig;
}

/**
Expand All @@ -114,7 +114,7 @@ export function ENSNodeConfigCardDisplay({ ensApiPublicConfig }: ENSNodeConfigCa
* Props for ENSNodeConfigInfoView - internal component that accepts props for testing/mocking
*/
export interface ENSNodeConfigInfoViewProps {
ensApiPublicConfig?: ENSApiPublicConfig;
ensApiPublicConfig?: EnsApiPublicConfig;
error?: ErrorInfoProps;
isLoading?: boolean;
}
Expand Down Expand Up @@ -147,28 +147,30 @@ export function ENSNodeConfigInfoView({
* ENSNodeConfigInfo component - fetches and displays ENSNode configuration data
*/
export function ENSNodeConfigInfo() {
const ensNodeConfigQuery = useENSNodeConfig();
const ensApiConfig = useEnsApiConfig();

return (
<ENSNodeConfigInfoView
ensApiPublicConfig={ensNodeConfigQuery.isSuccess ? ensNodeConfigQuery.data : undefined}
error={
ensNodeConfigQuery.isError
? {
title: "ENSNodeConfigInfo Error",
description: ensNodeConfigQuery.error.message,
}
: undefined
}
isLoading={ensNodeConfigQuery.isPending}
/>
);
if (ensApiConfig.isPending) {
return <ENSNodeConfigInfoView isLoading={true} />;
}

if (ensApiConfig.isError) {
return (
<ENSNodeConfigInfoView
error={{
title: "Failed to Load ENSNode Configuration",
description: ensApiConfig.error.message,
}}
/>
);
}

return <ENSNodeConfigInfoView ensApiPublicConfig={ensApiConfig.data} />;
}

function ENSNodeConfigCardContent({
ensApiPublicConfig,
}: {
ensApiPublicConfig: ENSApiPublicConfig;
ensApiPublicConfig: EnsApiPublicConfig;
}) {
const cardItemValueStyles = "text-sm leading-6 font-normal text-black";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,22 @@

import type { PropsWithChildren } from "react";

import { useENSNodeConfig } from "@ensnode/ensnode-react";

import { useEnsApiConfig } from "@/components/config/useEnsApiConfig";
import { ErrorInfo } from "@/components/error-info";
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 } = useENSNodeConfig();
const ensApiConfig = useEnsApiConfig();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, let's check the useEnsApiConfig hook to see how it's gated
echo "=== Checking useEnsApiConfig implementation ==="
if [ -f "apps/ensadmin/src/components/config/useEnsApiConfig.ts" ]; then
  cat -n "apps/ensadmin/src/components/config/useEnsApiConfig.ts"
else
  echo "File not found, searching for it..."
  find . -name "useEnsApiConfig.ts" -type f
fi

Repository: namehash/ensnode

Length of output: 837


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check RequireActiveConnection component
echo "=== Checking RequireActiveConnection implementation ==="
if [ -f "apps/ensadmin/src/components/connections/require-active-connection.tsx" ]; then
  cat -n "apps/ensadmin/src/components/connections/require-active-connection.tsx"
else
  echo "File not found, searching for it..."
  find . -name "require-active-connection.tsx" -type f
fi

Repository: namehash/ensnode

Length of output: 1284


Error UI is unreachable when indexing status fails during initial load.

RequireActiveConnection checks ensApiConfig.status to show error messages, but useEnsApiConfig has its query disabled when indexingStatus.isSuccess is false. When indexing fails before the first query execution, the status remains stuck at "pending" instead of transitioning to "error", leaving users with a permanent loading spinner. Either disable the component before calling the hook or derive error states directly from indexing status.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensadmin/src/components/connections/require-active-connection.tsx` at
line 13, RequireActiveConnection currently reads ensApiConfig.status from
useEnsApiConfig, but that hook is disabled when indexingStatus.isSuccess is
false so its status stays "pending" and the error UI is never shown; update
RequireActiveConnection to derive error/loading states from indexingStatus
directly (use indexingStatus.isError / indexingStatus.error to render the error
UI) or ensure useEnsApiConfig runs regardless of indexingStatus (remove the
query's enabled: false gating) so ensApiConfig.status can transition to "error";
locate the logic in RequireActiveConnection that uses ensApiConfig.status and
replace it with checks against indexingStatus (or re-enable the hook) and
propagate the indexing error into the same error rendering path.


if (status === "pending") return <Loading />;
if (ensApiConfig.status === "pending") return <Loading />;

if (status === "error") {
if (ensApiConfig.status === "error") {
return (
<section className="p-6">
<ErrorInfo title="Unable to parse ENSNode Config" description={error.message} />
<ErrorInfo title="Failed to connect to ENSApi" description={ensApiConfig.error.message} />
</section>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,75 +7,92 @@ import { useCallback, useMemo } from "react";
import {
createIndexingStatusQueryOptions,
QueryParameter,
useENSNodeSDKConfig,
useEnsApiProviderOptions,
type useIndexingStatus,
useSwrQuery,
WithSDKConfigParameter,
WithEnsApiProviderOptions,
} from "@ensnode/ensnode-react";
Comment on lines 7 to 14
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

type useIndexingStatus is imported but never referenced in a type position (it only appears in a JSDoc {@link ...}), which will trip Biome's unused-imports rule. Remove this import (or reference a real type) to keep lint passing.

Copilot uses AI. Check for mistakes.
import {
CrossChainIndexingStatusSnapshotOmnichain,
createRealtimeIndexingStatusProjection,
Duration,
type IndexingStatusRequest,
IndexingStatusResponseCodes,
IndexingStatusResponseOk,
EnsApiIndexingStatusRequest,
EnsApiIndexingStatusResponseCodes,
EnsApiIndexingStatusResponseOk,
EnsApiPublicConfig,
} from "@ensnode/ensnode-sdk";

const DEFAULT_REFETCH_INTERVAL = secondsToMilliseconds(10);

const REALTIME_PROJECTION_REFRESH_RATE: Duration = 1;

interface CachableIndexingStatus {
crossChainIndexingStatusSnapshot: CrossChainIndexingStatusSnapshotOmnichain;
config: EnsApiPublicConfig;
}

interface UseIndexingStatusParameters
extends IndexingStatusRequest,
QueryParameter<CrossChainIndexingStatusSnapshotOmnichain> {}
extends EnsApiIndexingStatusRequest,
QueryParameter<CachableIndexingStatus> {}

/**
* A proxy hook for {@link useIndexingStatus} which applies
* stale-while-revalidate cache for successful responses.
*/
export function useIndexingStatusWithSwr(
parameters: WithSDKConfigParameter & UseIndexingStatusParameters = {},
parameters: WithEnsApiProviderOptions & UseIndexingStatusParameters = {},
) {
const { config, query = {} } = parameters;
const _config = useENSNodeSDKConfig(config);
const { options, query = {} } = parameters;
const providerOptions = useEnsApiProviderOptions(options);
const now = useNow({ timeToRefresh: REALTIME_PROJECTION_REFRESH_RATE });

const queryOptions = useMemo(() => createIndexingStatusQueryOptions(_config), [_config]);
const queryOptions = useMemo(
() => createIndexingStatusQueryOptions(providerOptions),
[providerOptions],
);
const queryKey = useMemo(() => ["swr", ...queryOptions.queryKey], [queryOptions.queryKey]);
const queryFn = useCallback(
async () =>
queryOptions.queryFn().then(async (response) => {
// An indexing status response was successfully fetched,
// but the response code contained within the response was not 'ok'.
// Therefore, throw an error to avoid caching this response.
if (response.responseCode !== IndexingStatusResponseCodes.Ok) {
if (response.responseCode !== EnsApiIndexingStatusResponseCodes.Ok) {
throw new Error(
"Received Indexing Status response with responseCode other than 'ok' which will not be cached.",
);
}

// The indexing status snapshot has been fetched and successfully validated for caching.
// The indexing status snapshot, including ENSApi public config,
// has been fetched and successfully validated for caching.
// Therefore, return it so that query cache for `queryOptions.queryKey` will:
// - Replace the currently cached value (if any) with this new value.
// - Return this non-null value.
return response.realtimeProjection.snapshot;
return {
crossChainIndexingStatusSnapshot: response.realtimeProjection.snapshot,
config: response.config,
} satisfies CachableIndexingStatus;
}),
[queryOptions.queryFn],
);

// Call select function to `createRealtimeIndexingStatusProjection` each time
// `now` is updated.
const select = useCallback(
(cachedSnapshot: CrossChainIndexingStatusSnapshotOmnichain): IndexingStatusResponseOk => {
const realtimeProjection = createRealtimeIndexingStatusProjection(cachedSnapshot, now);
(cachedResult: CachableIndexingStatus): EnsApiIndexingStatusResponseOk => {
const realtimeProjection = createRealtimeIndexingStatusProjection(
cachedResult.crossChainIndexingStatusSnapshot,
now,
);

// Maintain the original response shape of `IndexingStatusResponse`
// Maintain the original response shape of `EnsApiIndexingStatusResponseOk`
// for the consumers. Creating a new projection from the cached snapshot
// each time `now` is updated should be implementation detail.
return {
responseCode: IndexingStatusResponseCodes.Ok,
responseCode: EnsApiIndexingStatusResponseCodes.Ok,
realtimeProjection,
} satisfies IndexingStatusResponseOk;
config: cachedResult.config,
} satisfies EnsApiIndexingStatusResponseOk;
},
[now],
);
Expand Down
6 changes: 3 additions & 3 deletions apps/ensadmin/src/components/layout-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { AppSidebar } from "@/components/app-sidebar";
import { RequireActiveConnection } from "@/components/connections/require-active-connection";
import { RequireSelectedConnection } from "@/components/connections/require-selected-connection";
import { Header, HeaderActions, HeaderBreadcrumbs, HeaderNav } from "@/components/header";
import { SelectedENSNodeProvider } from "@/components/providers/selected-ensnode-provider";
import { SelectedEnsApiProvider } from "@/components/providers/selected-ensnode-provider";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { Skeleton } from "@/components/ui/skeleton";

Expand Down Expand Up @@ -54,15 +54,15 @@ export function LayoutWrapper({
<AppSidebar />
</Suspense>
<SidebarInset className="min-w-0">
<SelectedENSNodeProvider>
<SelectedEnsApiProvider>
<Header>
<HeaderNav>
<HeaderBreadcrumbs>{breadcrumbs}</HeaderBreadcrumbs>
</HeaderNav>
<HeaderActions>{actions}</HeaderActions>
</Header>
<RequireActiveConnection>{children}</RequireActiveConnection>
</SelectedENSNodeProvider>
</SelectedEnsApiProvider>
</SidebarInset>
</SidebarProvider>
</RequireSelectedConnection>
Expand Down
Loading