diff --git a/.changeset/brown-readers-talk.md b/.changeset/brown-readers-talk.md new file mode 100644 index 0000000000..de2efcee7b --- /dev/null +++ b/.changeset/brown-readers-talk.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-sdk": minor +--- + +BREAKING: Removed DefaultRecordsSelection export: integrating apps should define their own set of records to request when using useRecords(). diff --git a/.changeset/nine-ducks-lick.md b/.changeset/nine-ducks-lick.md new file mode 100644 index 0000000000..c86240587c --- /dev/null +++ b/.changeset/nine-ducks-lick.md @@ -0,0 +1,5 @@ +--- +"ensadmin": minor +--- + +ENSAdmin now supports ENSApi Version info. diff --git a/.changeset/shaky-schools-nail.md b/.changeset/shaky-schools-nail.md new file mode 100644 index 0000000000..ffd51a53b6 --- /dev/null +++ b/.changeset/shaky-schools-nail.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-sdk": minor +--- + +BREAKING: client.config() now returns Promise instead of ENSIndexerPublicConfig. diff --git a/.changeset/sixty-onions-leave.md b/.changeset/sixty-onions-leave.md new file mode 100644 index 0000000000..542b2cf811 --- /dev/null +++ b/.changeset/sixty-onions-leave.md @@ -0,0 +1,5 @@ +--- +"ensadmin": minor +--- + +ENSAdmin now displays whether ENSNode attempted acceleration for an acceleratable endpoint in the Protocol Inspector. diff --git a/.changeset/young-badgers-trade.md b/.changeset/young-badgers-trade.md new file mode 100644 index 0000000000..812a843358 --- /dev/null +++ b/.changeset/young-badgers-trade.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-react": minor +--- + +BREAKING: `useENSNodeConfig` has been renamed to `useENSNodeSDKConfig`. `useENSIndexerConfig` has been renamed to `useENSNodeConfig`. diff --git a/apps/ensadmin/src/app/inspect/_components/render-requests-output.tsx b/apps/ensadmin/src/app/inspect/_components/render-requests-output.tsx index fcce26208e..2e3257124b 100644 --- a/apps/ensadmin/src/app/inspect/_components/render-requests-output.tsx +++ b/apps/ensadmin/src/app/inspect/_components/render-requests-output.tsx @@ -1,5 +1,5 @@ import type { UseQueryResult } from "@tanstack/react-query"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { type AcceleratableResponse, @@ -36,29 +36,23 @@ export function RenderRequestsOutput({ }) { const [tab, setTab] = useState("accelerated"); - // TODO: produce a diff between accelerated/not-accelerated and display any differences - const result = useMemo(() => { - if (tab === "accelerated" && accelerated.status === "success") { - return accelerated.data[dataKey]; - } + const focused = useMemo(() => { + if (tab === "accelerated") return accelerated; + if (tab === "unaccelerated") return unaccelerated; - if (tab === "unaccelerated" && unaccelerated.status === "success") { - return unaccelerated.data[dataKey]; - } - - return accelerated.data?.[dataKey] || unaccelerated.data?.[dataKey]; - }, [accelerated, unaccelerated, tab, dataKey]); + throw new Error("never"); + }, [accelerated, unaccelerated]); - const someError = accelerated.error || unaccelerated.error; + // need special derivation to capture refetching state + const acceleratedLoading = accelerated.isPending || accelerated.isRefetching; + const unacceleratedLoading = unaccelerated.isPending || unaccelerated.isRefetching; - // show major loading if either query is pending/refreshing - const showLoading = - (accelerated.isPending || accelerated.isRefetching) && - (unaccelerated.isPending || unaccelerated.isRefetching); + const acceleratedSuccess = !acceleratedLoading && !accelerated.isError; + const unacceleratedSuccess = !unacceleratedLoading && !unaccelerated.isError; const multipleDiff = useMemo(() => { - if (accelerated.status !== "success") return null; - if (unaccelerated.status !== "success") return null; + if (!acceleratedSuccess) return null; + if (!unacceleratedSuccess) return null; if (!accelerated.data.trace) return null; if (!unaccelerated.data.trace) return null; @@ -66,17 +60,21 @@ export function RenderRequestsOutput({ const acceleratedDuration = getTraceDuration(accelerated.data.trace); const unacceleratedDuration = getTraceDuration(unaccelerated.data.trace); - if (acceleratedDuration === 0) return null; + if (acceleratedDuration === 0) return null; // prevent division by zero... - const multiple = unacceleratedDuration / acceleratedDuration; - return multiple; + return unacceleratedDuration / acceleratedDuration; }, [accelerated, unaccelerated]); - if (showLoading) { - // if we're loading but there's no active fetch, the query is unable to be executed, so render null - if (accelerated.fetchStatus === "idle") return null; + useEffect(() => { + if (unacceleratedLoading) setTab("accelerated"); + }, [unacceleratedLoading, setTab]); + + // if we're loading but there's no active fetch, the query is unable to be executed, so render null + const isNotExecutable = acceleratedLoading && accelerated.fetchStatus === "idle"; + if (isNotExecutable) return null; - // otherwise, we're in-flight, render loading + // show major loading if accelerated query is pending/refreshing + if (acceleratedLoading) { return ( @@ -90,20 +88,21 @@ export function RenderRequestsOutput({ return ( <> + {/* Response Card */} ENSNode Response {(() => { - if (someError) { + if (focused.error) { return ( {JSON.stringify( { - message: someError.message, - ...(someError instanceof ClientError && - !!someError.details && { details: someError.details }), + message: focused.error.message, + ...(focused.error instanceof ClientError && + !!focused.error.details && { details: focused.error.details }), }, null, 2, @@ -114,13 +113,15 @@ export function RenderRequestsOutput({ return ( - {JSON.stringify(result, null, 2)} + {JSON.stringify(focused.data?.[dataKey], null, 2)} ); })()} - {!someError && (accelerated.data?.trace || unaccelerated.data?.trace) && ( + + {/* Execution Trace Card */} + {acceleratedSuccess && ( @@ -156,25 +157,24 @@ export function RenderRequestsOutput({ return null; })()} + Accelerated - {accelerated.data ? ( - `(${ - // biome-ignore lint/style/noNonNullAssertion: exists - renderTraceDuration(accelerated.data.trace!) - })` + {acceleratedSuccess && accelerated.data.trace ? ( + `(${renderTraceDuration(accelerated.data.trace)})` ) : ( )} - + Unaccelerated - {unaccelerated.data ? ( - `(${ - // biome-ignore lint/style/noNonNullAssertion: exists - renderTraceDuration(unaccelerated.data.trace!) - })` + {unacceleratedSuccess && unaccelerated.data.trace ? ( + `(${renderTraceDuration(unaccelerated.data.trace)})` ) : ( )} @@ -184,44 +184,14 @@ export function RenderRequestsOutput({ - {(() => { - switch (accelerated.status) { - case "pending": { - return ( -
- -
- ); - } - case "success": { - if (accelerated.data.trace) - return ; - throw new Error( - "Invariant: RenderRequestsOutput accelerated.data.trace is undefined.", - ); - } - } - })()} + {acceleratedSuccess && !!accelerated.data.trace && ( + + )}
- {(() => { - switch (unaccelerated.status) { - case "pending": { - return ( -
- -
- ); - } - case "success": { - if (unaccelerated.data.trace) - return ; - throw new Error( - "Invariant: RenderRequestsOutput unaccelerated.data.trace is undefined.", - ); - } - } - })()} + {unacceleratedSuccess && !!unaccelerated.data.trace && ( + + )}
diff --git a/apps/ensadmin/src/app/inspect/records/page.tsx b/apps/ensadmin/src/app/inspect/records/page.tsx index 893e93f4a8..caec8c8e33 100644 --- a/apps/ensadmin/src/app/inspect/records/page.tsx +++ b/apps/ensadmin/src/app/inspect/records/page.tsx @@ -5,7 +5,6 @@ import { useState } from "react"; import { useDebouncedValue } from "rooks"; import { useRecords } from "@ensnode/ensnode-react"; -import { DefaultRecordsSelection } from "@ensnode/ensnode-sdk"; import { RenderRequestsOutput } from "@/app/inspect/_components/render-requests-output"; import { Pill } from "@/components/pill"; @@ -13,6 +12,8 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { useActiveNamespace } from "@/hooks/active/use-active-namespace"; +import { DefaultRecordsSelection } from "@/lib/default-records-selection"; const EXAMPLE_INPUT = [ "vitalik.eth", @@ -30,8 +31,9 @@ const EXAMPLE_INPUT = [ // TODO: showcase current ENSNode configuration and viable acceleration pathways? // TODO: use shadcn/form, react-hook-form, and zod to make all of this nicer aross the board -// TODO: sync form state to query params, current just defaulting is supported +// TODO: sync form state to query params, currently just defaulting is supported export default function ResolveRecordsInspector() { + const namespace = useActiveNamespace(); const searchParams = useSearchParams(); const [name, setName] = useState(searchParams.get("name") || EXAMPLE_INPUT[0]); @@ -39,8 +41,7 @@ export default function ResolveRecordsInspector() { const canQuery = !!debouncedName && debouncedName.length > 0; - // TODO: switch on connected ensnode's configured namespace - const selection = DefaultRecordsSelection.mainnet; + const selection = DefaultRecordsSelection[namespace]; const accelerated = useRecords({ name: debouncedName, diff --git a/apps/ensadmin/src/app/mock/recent-registrations/page.tsx b/apps/ensadmin/src/app/mock/recent-registrations/page.tsx index f690213b34..fcc711102e 100644 --- a/apps/ensadmin/src/app/mock/recent-registrations/page.tsx +++ b/apps/ensadmin/src/app/mock/recent-registrations/page.tsx @@ -16,7 +16,6 @@ import { import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { ensIndexerPublicConfig } from "../config-api.mock"; import { indexingStatusResponseOkOmnichain } from "../indexing-status-api.mock"; type LoadingVariant = "Loading" | "Loading Error"; @@ -39,7 +38,7 @@ export default function MockRegistrationsPage() { return { error: { title: "RecentRegistrations Error", - description: "Failed to fetch ENSIndexerConfig or IndexingStatus.", + description: "Failed to fetch IndexingStatus.", }, } satisfies RecentRegistrationsErrorProps; @@ -61,7 +60,6 @@ export default function MockRegistrationsPage() { } return { - ensIndexerConfig: ensIndexerPublicConfig, realtimeProjection: indexingStatus.realtimeProjection, } satisfies RecentRegistrationsOkProps; } catch (error) { @@ -113,10 +111,7 @@ export default function MockRegistrationsPage() { {typeof props.error !== "undefined" ? ( ) : ( - + )} ); diff --git a/apps/ensadmin/src/app/name/_components/NameDetailPageContent.tsx b/apps/ensadmin/src/app/name/_components/NameDetailPageContent.tsx index 40b49e7829..b0b1340ab4 100644 --- a/apps/ensadmin/src/app/name/_components/NameDetailPageContent.tsx +++ b/apps/ensadmin/src/app/name/_components/NameDetailPageContent.tsx @@ -1,10 +1,11 @@ "use client"; import { ASSUME_IMMUTABLE_QUERY, useRecords } from "@ensnode/ensnode-react"; -import { getCommonCoinTypes, type Name, type ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; +import { type Name, type ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; import { Card, CardContent } from "@/components/ui/card"; import { useActiveNamespace } from "@/hooks/active/use-active-namespace"; +import { getCommonCoinTypes } from "@/lib/default-records-selection"; import { AdditionalRecords } from "./AdditionalRecords"; import { Addresses } from "./Addresses"; diff --git a/apps/ensadmin/src/components/connection/config-info/config-info.tsx b/apps/ensadmin/src/components/connection/config-info/config-info.tsx index a1108faa0f..0a11419ad5 100644 --- a/apps/ensadmin/src/components/connection/config-info/config-info.tsx +++ b/apps/ensadmin/src/components/connection/config-info/config-info.tsx @@ -8,8 +8,8 @@ import { Replace } from "lucide-react"; import { ReactNode } from "react"; -import { useENSIndexerConfig } from "@ensnode/ensnode-react"; -import { ENSIndexerPublicConfig } from "@ensnode/ensnode-sdk"; +import { useENSNodeConfig } from "@ensnode/ensnode-react"; +import { type ENSApiPublicConfig, getENSRootChainId } from "@ensnode/ensnode-sdk"; import { ChainIcon } from "@/components/chains/ChainIcon"; import { ConfigInfoAppCard } from "@/components/connection/config-info/app-card"; @@ -84,16 +84,16 @@ function ENSNodeCardLoadingSkeleton() { * Props for ENSNodeConfigCardDisplay - display component that accepts props for testing/mocking */ export interface ENSNodeConfigCardDisplayProps { - ensIndexerConfig: ENSIndexerPublicConfig; + ensApiPublicConfig: ENSApiPublicConfig; } /** * Display component that receives props - used for reusable/mockable presentation */ -export function ENSNodeConfigCardDisplay({ ensIndexerConfig }: ENSNodeConfigCardDisplayProps) { +export function ENSNodeConfigCardDisplay({ ensApiPublicConfig }: ENSNodeConfigCardDisplayProps) { return ( - + ); } @@ -102,7 +102,7 @@ export function ENSNodeConfigCardDisplay({ ensIndexerConfig }: ENSNodeConfigCard * Props for ENSNodeConfigInfoView - internal component that accepts props for testing/mocking */ export interface ENSNodeConfigInfoViewProps { - ensIndexerConfig?: ENSIndexerPublicConfig; + ensApiPublicConfig?: ENSApiPublicConfig; error?: ErrorInfoProps; isLoading?: boolean; } @@ -111,7 +111,7 @@ export interface ENSNodeConfigInfoViewProps { * Internal view component that accepts props - used by both the main component and mock pages */ export function ENSNodeConfigInfoView({ - ensIndexerConfig, + ensApiPublicConfig, error, isLoading = false, }: ENSNodeConfigInfoViewProps) { @@ -120,7 +120,7 @@ export function ENSNodeConfigInfoView({ } // Show ENSNode card - shell with skeleton while loading, or content when ready - if (isLoading || !ensIndexerConfig) { + if (isLoading || !ensApiPublicConfig) { return ( @@ -128,42 +128,44 @@ export function ENSNodeConfigInfoView({ ); } - return ; + return ; } /** * ENSNodeConfigInfo component - fetches and displays ENSNode configuration data */ export function ENSNodeConfigInfo() { - const ensIndexerConfigQuery = useENSIndexerConfig(); + const ensNodeConfigQuery = useENSNodeConfig(); return ( ); } function ENSNodeConfigCardContent({ - ensIndexerConfig, + ensApiPublicConfig, }: { - ensIndexerConfig: ENSIndexerPublicConfig; + ensApiPublicConfig: ENSApiPublicConfig; }) { const cardItemValueStyles = "text-sm leading-6 font-normal text-black"; - const healReverseAddressesActivated = !ensIndexerConfig.isSubgraphCompatible; - const indexAdditionalRecordsActivated = !ensIndexerConfig.isSubgraphCompatible; - const replaceUnnormalizedLabelsActivated = !ensIndexerConfig.isSubgraphCompatible; - const subgraphCompatibilityActivated = ensIndexerConfig.isSubgraphCompatible; + const { ensIndexerPublicConfig } = ensApiPublicConfig; + + const healReverseAddressesActivated = !ensIndexerPublicConfig.isSubgraphCompatible; + const indexAdditionalRecordsActivated = !ensIndexerPublicConfig.isSubgraphCompatible; + const replaceUnnormalizedLabelsActivated = !ensIndexerPublicConfig.isSubgraphCompatible; + const subgraphCompatibilityActivated = ensIndexerPublicConfig.isSubgraphCompatible; const healReverseAddressesDescription = healReverseAddressesActivated ? (

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

@@ -212,6 +214,8 @@ function ENSNodeConfigCardContent({

); + const ensRootChainId = getENSRootChainId(ensIndexerPublicConfig.namespace); + return ( <> {/*ENSDb*/} @@ -225,7 +229,9 @@ function ENSNodeConfigCardContent({ }, { label: "Database Schema", - value:

{ensIndexerConfig.databaseSchemaName}

, + value: ( +

{ensIndexerPublicConfig.databaseSchemaName}

+ ), additionalInfo: (

ENSIndexer writes indexed data to tables within this Postgres database schema.

), @@ -233,7 +239,7 @@ function ENSNodeConfigCardContent({ ]} version={

- v{ensIndexerConfig.versionInfo.ensDb} + v{ensIndexerPublicConfig.versionInfo.ensDb}

} docsLink={new URL("https://ensnode.io/ensdb/")} @@ -246,12 +252,14 @@ function ENSNodeConfigCardContent({ items={[ { label: "Node.js", - value:

{ensIndexerConfig.versionInfo.nodejs}

, + value: ( +

{ensIndexerPublicConfig.versionInfo.nodejs}

+ ), additionalInfo: (

Version of the{" "} Node.js {" "} @@ -261,12 +269,14 @@ function ENSNodeConfigCardContent({ }, { label: "Ponder", - value:

{ensIndexerConfig.versionInfo.ponder}

, + value: ( +

{ensIndexerPublicConfig.versionInfo.ponder}

+ ), additionalInfo: (

Version of the{" "} ponder {" "} @@ -277,13 +287,15 @@ function ENSNodeConfigCardContent({ { label: "ens-normalize.js", value: ( -

{ensIndexerConfig.versionInfo.ensNormalize}

+

+ {ensIndexerPublicConfig.versionInfo.ensNormalize} +

), additionalInfo: (

Version of the{" "} @adraffy/ens-normalize {" "} @@ -296,7 +308,8 @@ function ENSNodeConfigCardContent({ value: (

  • - {ensIndexerConfig.labelSet.labelSetId}:{ensIndexerConfig.labelSet.labelSetVersion} + {ensIndexerPublicConfig.labelSet.labelSetId}: + {ensIndexerPublicConfig.labelSet.labelSetVersion}
), @@ -315,14 +328,14 @@ function ENSNodeConfigCardContent({ }, { label: "ENS Namespace", - value:

{ensIndexerConfig.namespace}

, + value:

{ensIndexerPublicConfig.namespace}

, additionalInfo:

The ENS namespace that ENSNode operates in the context of.

, }, { label: "Indexed Chains", value: (
- {Array.from(ensIndexerConfig.indexedChainIds).map((chainId) => ( + {Array.from(ensIndexerPublicConfig.indexedChainIds).map((chainId) => ( @@ -342,7 +355,7 @@ function ENSNodeConfigCardContent({ label: "Plugins", value: (
- {ensIndexerConfig.plugins.map((plugin) => ( + {ensIndexerPublicConfig.plugins.map((plugin) => ( - v{ensIndexerConfig.versionInfo.ensIndexer} + v{ensIndexerPublicConfig.versionInfo.ensIndexer}

} docsLink={new URL("https://ensnode.io/ensindexer/")} @@ -399,7 +412,8 @@ function ENSNodeConfigCardContent({ label: "Server LabelSet", value: (

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

), additionalInfo: ( @@ -416,7 +430,7 @@ function ENSNodeConfigCardContent({ ]} version={

- v{ensIndexerConfig.versionInfo.ensRainbow} + v{ensIndexerPublicConfig.versionInfo.ensRainbow}

} docsLink={new URL("https://ensnode.io/ensrainbow/")} diff --git a/apps/ensadmin/src/components/connection/index.tsx b/apps/ensadmin/src/components/connection/index.tsx index d714f69ca9..c71a88f171 100644 --- a/apps/ensadmin/src/components/connection/index.tsx +++ b/apps/ensadmin/src/components/connection/index.tsx @@ -1,11 +1,12 @@ "use client"; +import packageJson from "@/../package.json" with { type: "json" }; + import { PlugZap } from "lucide-react"; import { ENSNodeConfigInfo } from "@/components/connection/config-info"; import { ConfigInfoAppCard } from "@/components/connection/config-info/app-card"; import { CopyButton } from "@/components/copy-button"; -import { ENSAdminVersion } from "@/components/ensadmin-version"; import { ENSAdminIcon } from "@/components/icons/ensnode-apps/ensadmin-icon"; import { useSelectedConnection } from "@/hooks/active/use-selected-connection"; @@ -26,7 +27,11 @@ export default function ConnectionInfo() { } - version={} + version={ +

+ v{packageJson.version} +

+ } docsLink={new URL("https://ensnode.io/ensadmin/")} /> diff --git a/apps/ensadmin/src/components/connections/connections-library-selector.tsx b/apps/ensadmin/src/components/connections/connections-library-selector.tsx index be9fe64e3c..7eb69b71af 100644 --- a/apps/ensadmin/src/components/connections/connections-library-selector.tsx +++ b/apps/ensadmin/src/components/connections/connections-library-selector.tsx @@ -73,7 +73,7 @@ export function ConnectionsLibrarySelector() { } else if (!selectedConnection.validatedSelectedConnection.isValid) { connectionMessage = "Invalid connection"; } else { - connectionMessage = "Select ENSNode"; + connectionMessage = selectedConnection.validatedSelectedConnection.url.href; } const serverConnections = connectionLibrary.filter((connection) => connection.type === "server"); diff --git a/apps/ensadmin/src/components/connections/require-active-connection.tsx b/apps/ensadmin/src/components/connections/require-active-connection.tsx index 5f0190f76b..683f4308a7 100644 --- a/apps/ensadmin/src/components/connections/require-active-connection.tsx +++ b/apps/ensadmin/src/components/connections/require-active-connection.tsx @@ -2,7 +2,7 @@ import type { PropsWithChildren } from "react"; -import { useENSIndexerConfig } from "@ensnode/ensnode-react"; +import { useENSNodeConfig } from "@ensnode/ensnode-react"; import { ErrorInfo } from "@/components/error-info"; import { LoadingSpinner } from "@/components/loading-spinner"; @@ -11,7 +11,7 @@ import { LoadingSpinner } from "@/components/loading-spinner"; * Allows consumers to use `useActiveConnection` by blocking rendering until it is available. */ export function RequireActiveConnection({ children }: PropsWithChildren) { - const { status, error } = useENSIndexerConfig(); + const { status, error } = useENSNodeConfig(); if (status === "pending") return ; diff --git a/apps/ensadmin/src/components/ensadmin-version.tsx b/apps/ensadmin/src/components/ensadmin-version.tsx deleted file mode 100644 index 4416b492e8..0000000000 --- a/apps/ensadmin/src/components/ensadmin-version.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -import { Skeleton } from "@/components/ui/skeleton"; -import { ensAdminVersion } from "@/lib/env"; - -export function ENSAdminVersion() { - const [version, setVersion] = useState(null); - - useEffect(() => { - ensAdminVersion().then(setVersion); - }, []); - - if (version === null) { - return ; - } - - return ( - v{version} - ); -} diff --git a/apps/ensadmin/src/components/recent-registrations/components.tsx b/apps/ensadmin/src/components/recent-registrations/components.tsx index e151e62db6..dd8a9c6078 100644 --- a/apps/ensadmin/src/components/recent-registrations/components.tsx +++ b/apps/ensadmin/src/components/recent-registrations/components.tsx @@ -5,7 +5,6 @@ import Link from "next/link"; import { Fragment } from "react"; import { - type ENSIndexerPublicConfig, type OmnichainIndexingStatusId, OmnichainIndexingStatusIds, type OmnichainIndexingStatusSnapshot, @@ -41,7 +40,6 @@ const SUPPORTED_OMNICHAIN_INDEXING_STATUSES: OmnichainIndexingStatusId[] = [ ]; export interface RecentRegistrationsOkProps { - ensIndexerConfig: ENSIndexerPublicConfig | undefined; realtimeProjection: RealtimeIndexingStatusProjection | undefined; maxRecords?: number; } @@ -54,18 +52,15 @@ export interface RecentRegistrationsErrorProps { * RecentRegistrations display variations: * * Standard - - * ensIndexerConfig: {@link ENSIndexerPublicConfig}, * indexingStatus: {@link OmnichainIndexingStatusSnapshotCompleted} | * {@link OmnichainIndexingStatusSnapshotFollowing}, * * UnsupportedOmnichainIndexingStatusMessage - - * ensIndexerConfig: {@link ENSIndexerPublicConfig}, * indexingStatus: {@link OmnichainIndexingStatusSnapshot} other than * {@link OmnichainIndexingStatusSnapshotCompleted} | * {@link OmnichainIndexingStatusSnapshotFollowing}, * * Loading - - * ensIndexerConfig: undefined, * indexingStatus: undefined, * * Error - @@ -84,9 +79,9 @@ export function RecentRegistrations(props: RecentRegistrationsProps) { return ; } - const { ensIndexerConfig, realtimeProjection, maxRecords = DEFAULT_MAX_RECORDS } = props; + const { realtimeProjection, maxRecords = DEFAULT_MAX_RECORDS } = props; - if (ensIndexerConfig === undefined || realtimeProjection === undefined) { + if (realtimeProjection === undefined) { return ; } diff --git a/apps/ensadmin/src/components/recent-registrations/hooks.ts b/apps/ensadmin/src/components/recent-registrations/hooks.ts index 2496126768..28c0e2c0b8 100644 --- a/apps/ensadmin/src/components/recent-registrations/hooks.ts +++ b/apps/ensadmin/src/components/recent-registrations/hooks.ts @@ -6,7 +6,6 @@ import { deserializeUnixTimestamp, type Name, type UnixTimestamp } from "@ensnod import { useActiveNamespace } from "@/hooks/active/use-active-namespace"; import { useSelectedConnection } from "@/hooks/active/use-selected-connection"; -import { ensAdminVersion } from "@/lib/env"; import { getNameWrapperAddress } from "@/lib/namespace-utils"; import type { Registration } from "./types"; @@ -126,10 +125,7 @@ async function fetchRecentRegistrations( const response = await fetch(new URL(`/subgraph`, ensNodeUrl), { method: "POST", - headers: { - "content-type": "application/json", - "x-ensadmin-version": await ensAdminVersion(), - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query }), }); diff --git a/apps/ensadmin/src/components/recent-registrations/registrations.tsx b/apps/ensadmin/src/components/recent-registrations/registrations.tsx index ce5ca20449..6b8e91325d 100644 --- a/apps/ensadmin/src/components/recent-registrations/registrations.tsx +++ b/apps/ensadmin/src/components/recent-registrations/registrations.tsx @@ -1,52 +1,27 @@ "use client"; -import { useENSIndexerConfig, useIndexingStatus } from "@ensnode/ensnode-react"; +import { useIndexingStatus } from "@ensnode/ensnode-react"; import { IndexingStatusResponseCodes } from "@ensnode/ensnode-sdk"; import { RecentRegistrations } from "@/components/recent-registrations/components"; export function Registrations() { - const ensIndexerConfigQuery = useENSIndexerConfig(); - const indexingStatusQuery = useIndexingStatus(); + const { status, data: indexingStatus, error } = useIndexingStatus(); - if (ensIndexerConfigQuery.isError) { + if (status === "pending") { return (
- -
- ); - } - - if (indexingStatusQuery.isError) { - return ( -
- +
); } - if (!ensIndexerConfigQuery.isSuccess || !indexingStatusQuery.isSuccess) { + if (status === "error") { return ( -
- {" "} - {/*display loading state*/} -
+ ); } - const ensIndexerConfig = ensIndexerConfigQuery.data; - const indexingStatus = indexingStatusQuery.data; - // even though indexing status was fetched successfully, // it can still refer to a server-side error if (indexingStatus.responseCode === IndexingStatusResponseCodes.Error) { @@ -62,12 +37,5 @@ export function Registrations() { ); } - return ( -
- -
- ); + return ; } diff --git a/apps/ensadmin/src/hooks/active/use-active-connection.tsx b/apps/ensadmin/src/hooks/active/use-active-connection.tsx index c6358ac0e8..e1fa783caf 100644 --- a/apps/ensadmin/src/hooks/active/use-active-connection.tsx +++ b/apps/ensadmin/src/hooks/active/use-active-connection.tsx @@ -1,6 +1,6 @@ "use client"; -import { useENSIndexerConfig } from "@ensnode/ensnode-react"; +import { useENSNodeConfig } from "@ensnode/ensnode-react"; /** * Hook to get the currently active ENSNode connection synchronously. @@ -16,7 +16,7 @@ import { useENSIndexerConfig } from "@ensnode/ensnode-react"; * @throws Error if no active ENSNode connection is available */ export function useActiveConnection() { - const { data } = useENSIndexerConfig(); + const { data } = useENSNodeConfig(); if (data === undefined) { throw new Error(`Invariant(useActiveConnection): Expected an active ENSNode Config`); diff --git a/apps/ensadmin/src/hooks/active/use-active-namespace.ts b/apps/ensadmin/src/hooks/active/use-active-namespace.ts index 9acbe53f1a..486417e7c8 100644 --- a/apps/ensadmin/src/hooks/active/use-active-namespace.ts +++ b/apps/ensadmin/src/hooks/active/use-active-namespace.ts @@ -13,4 +13,4 @@ import { useActiveConnection } from "./use-active-connection"; * @returns The namespace from the active ENSNode configuration * @throws Error if no active ENSNode Config is available */ -export const useActiveNamespace = () => useActiveConnection().namespace; +export const useActiveNamespace = () => useActiveConnection().ensIndexerPublicConfig.namespace; diff --git a/apps/ensadmin/src/hooks/async/use-namespace.ts b/apps/ensadmin/src/hooks/async/use-namespace.ts index 952ae0a3bd..be3a0925ed 100644 --- a/apps/ensadmin/src/hooks/async/use-namespace.ts +++ b/apps/ensadmin/src/hooks/async/use-namespace.ts @@ -1,4 +1,4 @@ -import { useENSIndexerConfig } from "@ensnode/ensnode-react"; +import { useENSNodeConfig } from "@ensnode/ensnode-react"; /** * Hook to get the namespace ID from the active ENSNode connection. @@ -22,10 +22,10 @@ import { useENSIndexerConfig } from "@ensnode/ensnode-react"; * ``` */ export function useNamespace() { - const query = useENSIndexerConfig(); + const query = useENSNodeConfig(); return { ...query, - data: query.data?.namespace ?? null, + data: query.data?.ensIndexerPublicConfig.namespace ?? null, }; } diff --git a/packages/ensnode-sdk/src/resolution/default-records-selection.ts b/apps/ensadmin/src/lib/default-records-selection.ts similarity index 73% rename from packages/ensnode-sdk/src/resolution/default-records-selection.ts rename to apps/ensadmin/src/lib/default-records-selection.ts index 4c0cc04a65..80e59d99d9 100644 --- a/packages/ensnode-sdk/src/resolution/default-records-selection.ts +++ b/apps/ensadmin/src/lib/default-records-selection.ts @@ -4,10 +4,13 @@ import { ENSNamespaceIds, maybeGetDatasource, } from "@ensnode/datasources"; - -import { type CoinType, ETH_COIN_TYPE, evmChainIdToCoinType } from "../ens"; -import { uniq } from "../shared"; -import type { ResolverRecordsSelection } from "./resolver-records-selection"; +import { + CoinType, + ETH_COIN_TYPE, + evmChainIdToCoinType, + ResolverRecordsSelection, + uniq, +} from "@ensnode/ensnode-sdk"; const getENSIP19SupportedCoinTypes = (namespace: ENSNamespaceId) => uniq( @@ -37,11 +40,9 @@ const TEXTS = [ "com.github", ] as const satisfies string[]; -// TODO: Phase out this concept. All apps should define their own selection of records. -// Additionally, we should update `useRecords` so that it can return not only all the -// (texts / addresses) records that are explicitly requested, but also any other (texts / addresses) -// records that ENSNode has found onchain. -// see: https://github.com/namehash/ensnode/issues/1084 +/** + * Defines a set of 'default' records to query when making Protocol Inspector requests. + */ export const DefaultRecordsSelection = { [ENSNamespaceIds.Mainnet]: { addresses: getCommonCoinTypes(ENSNamespaceIds.Mainnet), diff --git a/apps/ensadmin/src/lib/env.ts b/apps/ensadmin/src/lib/env.ts index e9d5cae92c..8d30c082d8 100644 --- a/apps/ensadmin/src/lib/env.ts +++ b/apps/ensadmin/src/lib/env.ts @@ -125,9 +125,3 @@ export function getServerConnectionLibrary(): HttpHostname[] { return uniqueConnections; } - -export async function ensAdminVersion(): Promise { - const packageJson = await import("@/../package.json"); - - return packageJson.version; -} diff --git a/apps/ensapi/.env.local.example b/apps/ensapi/.env.local.example index aef395b3a6..b967d7624e 100644 --- a/apps/ensapi/.env.local.example +++ b/apps/ensapi/.env.local.example @@ -53,3 +53,8 @@ DATABASE_URL=postgresql://dbuser:abcd1234@localhost:5432/my_database # RPC_URL_11155111=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY # RPC_URL_17000=https://eth-holesky.g.alchemy.com/v2/YOUR_API_KEY # RPC_URL_1337=http://localhost:8545 + +# Log Level +# Optional. If this is not set, the default value is "info". +# Allowed values: "fatal", "error", "warn", "info", "debug", "trace", "silent". +# LOG_LEVEL=info diff --git a/apps/ensapi/package.json b/apps/ensapi/package.json index 64b70560db..2289740e6e 100644 --- a/apps/ensapi/package.json +++ b/apps/ensapi/package.json @@ -44,6 +44,7 @@ "p-memoize": "^8.0.0", "p-reflect": "^3.1.0", "p-retry": "^7.1.0", + "pino": "^10.1.0", "ponder": "catalog:", "ponder-enrich-gql-docs-middleware": "^0.1.3", "viem": "catalog:", @@ -52,6 +53,7 @@ "devDependencies": { "@ensnode/shared-configs": "workspace:*", "@types/node": "catalog:", + "pino-pretty": "^13.1.2", "tsx": "^4.7.1", "typescript": "catalog:", "vitest": "catalog:" diff --git a/apps/ensapi/src/config/config.schema.test.ts b/apps/ensapi/src/config/config.schema.test.ts index 721a2de00c..7ca3b42bdd 100644 --- a/apps/ensapi/src/config/config.schema.test.ts +++ b/apps/ensapi/src/config/config.schema.test.ts @@ -9,7 +9,7 @@ import { } from "@ensnode/ensnode-sdk"; import type { RpcConfig } from "@ensnode/ensnode-sdk/internal"; -import { buildConfigFromEnvironment } from "@/config/config.schema"; +import { buildConfigFromEnvironment, buildEnsApiPublicConfig } from "@/config/config.schema"; import { ENSApi_DEFAULT_PORT } from "@/config/defaults"; import type { EnsApiEnvironment } from "@/config/environment"; @@ -73,3 +73,61 @@ describe("buildConfigFromEnvironment", () => { }); }); }); + +describe("buildEnsApiPublicConfig", () => { + it("returns a valid ENSApi public config with correct structure", () => { + const mockConfig = { + port: ENSApi_DEFAULT_PORT, + databaseUrl: BASE_ENV.DATABASE_URL, + ensIndexerUrl: new URL(BASE_ENV.ENSINDEXER_URL), + ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG, + namespace: ENSINDEXER_PUBLIC_CONFIG.namespace, + databaseSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName, + rpcConfigs: new Map([ + [ + 1, + { + httpRPCs: [new URL(VALID_RPC_URL)], + websocketRPC: undefined, + } satisfies RpcConfig, + ], + ]), + }; + + const result = buildEnsApiPublicConfig(mockConfig); + + expect(result).toStrictEqual({ + version: packageJson.version, + ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG, + }); + }); + + it("preserves the complete ENSIndexer public config structure", () => { + const mockConfig = { + port: ENSApi_DEFAULT_PORT, + databaseUrl: BASE_ENV.DATABASE_URL, + ensIndexerUrl: new URL(BASE_ENV.ENSINDEXER_URL), + ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG, + namespace: ENSINDEXER_PUBLIC_CONFIG.namespace, + databaseSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName, + rpcConfigs: new Map(), + }; + + const result = buildEnsApiPublicConfig(mockConfig); + + // Verify that all ENSIndexer public config fields are preserved + expect(result.ensIndexerPublicConfig.namespace).toBe(ENSINDEXER_PUBLIC_CONFIG.namespace); + expect(result.ensIndexerPublicConfig.plugins).toEqual(ENSINDEXER_PUBLIC_CONFIG.plugins); + expect(result.ensIndexerPublicConfig.versionInfo).toEqual(ENSINDEXER_PUBLIC_CONFIG.versionInfo); + expect(result.ensIndexerPublicConfig.indexedChainIds).toEqual( + ENSINDEXER_PUBLIC_CONFIG.indexedChainIds, + ); + expect(result.ensIndexerPublicConfig.isSubgraphCompatible).toBe( + ENSINDEXER_PUBLIC_CONFIG.isSubgraphCompatible, + ); + expect(result.ensIndexerPublicConfig.labelSet).toEqual(ENSINDEXER_PUBLIC_CONFIG.labelSet); + expect(result.ensIndexerPublicConfig.databaseSchemaName).toBe( + ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName, + ); + }); +}); diff --git a/apps/ensapi/src/config/config.schema.ts b/apps/ensapi/src/config/config.schema.ts index c1b3fa40cd..c5e1bfe39b 100644 --- a/apps/ensapi/src/config/config.schema.ts +++ b/apps/ensapi/src/config/config.schema.ts @@ -1,7 +1,9 @@ +import packageJson from "@/../package.json" with { type: "json" }; + import pRetry from "p-retry"; import { prettifyError, ZodError, z } from "zod/v4"; -import { ENSNodeClient, serializeENSIndexerPublicConfig } from "@ensnode/ensnode-sdk"; +import { type ENSApiPublicConfig, serializeENSIndexerPublicConfig } from "@ensnode/ensnode-sdk"; import { buildRpcConfigsFromEnv, DatabaseSchemaNameSchema, @@ -17,6 +19,8 @@ import { import { ENSApi_DEFAULT_PORT } from "@/config/defaults"; import type { EnsApiEnvironment } from "@/config/environment"; import { invariant_ensIndexerPublicConfigVersionInfo } from "@/config/validations"; +import { fetchENSIndexerConfig } from "@/lib/fetch-ensindexer-config"; +import logger from "@/lib/logger"; const EnsApiConfigSchema = z .object({ @@ -42,12 +46,11 @@ export type EnsApiConfig = z.infer; export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promise { try { const ensIndexerUrl = EnsIndexerUrlSchema.parse(env.ENSINDEXER_URL); - const client = new ENSNodeClient({ url: ensIndexerUrl }); - const ensIndexerPublicConfig = await pRetry(() => client.config(), { + const ensIndexerPublicConfig = await pRetry(() => fetchENSIndexerConfig(ensIndexerUrl), { retries: 3, onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => { - console.log( + logger.info( `ENSIndexer Config fetch attempt ${attemptNumber} failed (${error.message}). ${retriesLeft} retries left.`, ); }, @@ -67,13 +70,29 @@ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promis }); } catch (error) { if (error instanceof ZodError) { - throw new Error(`Failed to parse environment configuration: \n${prettifyError(error)}\n`); + logger.error(`Failed to parse environment configuration: \n${prettifyError(error)}\n`); + process.exit(1); } if (error instanceof Error) { - error.message = `Failed to build EnsApiConfig: ${error.message}`; + logger.error(error, `Failed to build EnsApiConfig`); + process.exit(1); } - throw error; + logger.error(`Unknown Error`); + process.exit(1); } } + +/** + * Builds the ENSApi public configuration from an EnsApiConfig object. + * + * @param config - The validated EnsApiConfig object + * @returns A complete ENSApiPublicConfig object + */ +export function buildEnsApiPublicConfig(config: EnsApiConfig): ENSApiPublicConfig { + return { + version: packageJson.version, + ensIndexerPublicConfig: config.ensIndexerPublicConfig, + }; +} diff --git a/apps/ensapi/src/config/environment.ts b/apps/ensapi/src/config/environment.ts index d69cbc624a..a02ac785cc 100644 --- a/apps/ensapi/src/config/environment.ts +++ b/apps/ensapi/src/config/environment.ts @@ -1,6 +1,7 @@ import type { DatabaseEnvironment, EnsIndexerUrlEnvironment, + LogLevelEnvironment, PortEnvironment, RpcEnvironment, } from "@ensnode/ensnode-sdk/internal"; @@ -15,4 +16,5 @@ import type { export type EnsApiEnvironment = Omit & EnsIndexerUrlEnvironment & RpcEnvironment & - PortEnvironment; + PortEnvironment & + LogLevelEnvironment; diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index c858675971..2fd9ad89c5 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -3,19 +3,21 @@ import config from "@/config"; import { IndexingStatusResponseCodes, type IndexingStatusResponseError, - serializeENSIndexerPublicConfig, + serializeENSApiPublicConfig, serializeIndexingStatusResponse, } from "@ensnode/ensnode-sdk"; +import { buildEnsApiPublicConfig } from "@/config/config.schema"; import { factory } from "@/lib/hono-factory"; import resolutionApi from "./resolution-api"; const app = factory.createApp(); -// include ENSIndexer Public Config endpoint +// include ENSApi Public Config endpoint app.get("/config", async (c) => { - return c.json(serializeENSIndexerPublicConfig(config.ensIndexerPublicConfig)); + const ensApiPublicConfig = buildEnsApiPublicConfig(config); + return c.json(serializeENSApiPublicConfig(ensApiPublicConfig)); }); // include ENSIndexer Indexing Status endpoint diff --git a/apps/ensapi/src/handlers/resolution-api.ts b/apps/ensapi/src/handlers/resolution-api.ts index 68cad3ffe6..45cc15cde1 100644 --- a/apps/ensapi/src/handlers/resolution-api.ts +++ b/apps/ensapi/src/handlers/resolution-api.ts @@ -6,7 +6,6 @@ import type { ResolveRecordsResponse, } from "@ensnode/ensnode-sdk"; -import { errorResponse } from "@/lib/handlers/error-response"; import { params } from "@/lib/handlers/params.schema"; import { validate } from "@/lib/handlers/validate"; import { factory } from "@/lib/hono-factory"; @@ -51,24 +50,19 @@ app.get( const { selection, trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; - try { - const { result, trace } = await captureTrace(() => - resolveForward(name, selection, { accelerate, canAccelerate }), - ); + const { result, trace } = await captureTrace(() => + resolveForward(name, selection, { accelerate, canAccelerate }), + ); - const response = { - records: result, + const response = { + records: result, - accelerationRequested: accelerate, - accelerationAttempted: accelerate && canAccelerate, - ...(showTrace && { trace }), - } satisfies ResolveRecordsResponse; + accelerationRequested: accelerate, + accelerationAttempted: accelerate && canAccelerate, + ...(showTrace && { trace }), + } satisfies ResolveRecordsResponse; - return c.json(response); - } catch (error) { - console.error(error); - return errorResponse(c, error); - } + return c.json(response); }, ); @@ -99,24 +93,19 @@ app.get( const { trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; - try { - const { result, trace } = await captureTrace(() => - resolveReverse(address, chainId, { accelerate, canAccelerate }), - ); + const { result, trace } = await captureTrace(() => + resolveReverse(address, chainId, { accelerate, canAccelerate }), + ); - const response = { - name: result, + const response = { + name: result, - accelerationRequested: accelerate, - accelerationAttempted: accelerate && canAccelerate, - ...(showTrace && { trace }), - } satisfies ResolvePrimaryNameResponse; + accelerationRequested: accelerate, + accelerationAttempted: accelerate && canAccelerate, + ...(showTrace && { trace }), + } satisfies ResolvePrimaryNameResponse; - return c.json(response); - } catch (error) { - console.error(error); - return errorResponse(c, error); - } + return c.json(response); }, ); @@ -145,24 +134,19 @@ app.get( const { chainIds, trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; - try { - const { result, trace } = await captureTrace(() => - resolvePrimaryNames(address, chainIds, { accelerate, canAccelerate }), - ); + const { result, trace } = await captureTrace(() => + resolvePrimaryNames(address, chainIds, { accelerate, canAccelerate }), + ); - const response = { - names: result, + const response = { + names: result, - accelerationRequested: accelerate, - accelerationAttempted: accelerate && canAccelerate, - ...(showTrace && { trace }), - } satisfies ResolvePrimaryNamesResponse; + accelerationRequested: accelerate, + accelerationAttempted: accelerate && canAccelerate, + ...(showTrace && { trace }), + } satisfies ResolvePrimaryNamesResponse; - return c.json(response); - } catch (error) { - console.error(error); - return errorResponse(c, error); - } + return c.json(response); }, ); diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index c0a547d763..07f1e4e330 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -10,6 +10,7 @@ import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; import { redactEnsApiConfig } from "@/config/redact"; import { errorResponse } from "@/lib/handlers/error-response"; import { factory } from "@/lib/hono-factory"; +import logger from "@/lib/logger"; import { sdk } from "@/lib/tracing/instrumentation"; import { canAccelerateMiddleware } from "@/middleware/can-accelerate.middleware"; import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; @@ -51,7 +52,7 @@ app.get("/health", async (c) => { // log hono errors to console app.onError((error, ctx) => { - console.error(error); + logger.error(error); return errorResponse(ctx, "Internal Server Error"); }); @@ -65,8 +66,9 @@ const server = serve( port: config.port, }, async (info) => { - console.log(`ENSApi listening on port ${info.port} with config:`); - console.log(prettyPrintJson(redactEnsApiConfig(config))); + logger.info( + `ENSApi listening on port ${info.port} with config:\n${prettyPrintJson(redactEnsApiConfig(config))}`, + ); // self-healthcheck to connect to ENSIndexer & warm Indexing Status / Can Accelerate cache await app.request("/health"); @@ -90,7 +92,7 @@ const gracefulShutdown = async () => { process.exit(0); } catch (error) { - console.error(error); + logger.error(error); process.exit(1); } }; @@ -100,6 +102,6 @@ process.on("SIGINT", gracefulShutdown); process.on("SIGTERM", gracefulShutdown); process.on("uncaughtException", async (error) => { - console.error(`Fatal Error:`, error); + logger.error(error, "uncaughtException"); await gracefulShutdown(); }); diff --git a/apps/ensapi/src/lib/fetch-ensindexer-config.ts b/apps/ensapi/src/lib/fetch-ensindexer-config.ts new file mode 100644 index 0000000000..0cd11beb8b --- /dev/null +++ b/apps/ensapi/src/lib/fetch-ensindexer-config.ts @@ -0,0 +1,17 @@ +import { + deserializeENSIndexerPublicConfig, + deserializeErrorResponse, + type SerializedENSIndexerPublicConfig, +} from "@ensnode/ensnode-sdk"; + +export async function fetchENSIndexerConfig(url: URL) { + const response = await fetch(new URL(`/api/config`, url)); + const responseData = await response.json(); + + if (!response.ok) { + const errorResponse = deserializeErrorResponse(responseData); + throw new Error(`Fetching ENSNode Config Failed: ${errorResponse.message}`); + } + + return deserializeENSIndexerPublicConfig(responseData as SerializedENSIndexerPublicConfig); +} diff --git a/apps/ensapi/src/lib/logger.ts b/apps/ensapi/src/lib/logger.ts new file mode 100644 index 0000000000..50c2642a63 --- /dev/null +++ b/apps/ensapi/src/lib/logger.ts @@ -0,0 +1,19 @@ +import pino from "pino"; + +import { getLogLevelFromEnv, type LogLevel } from "@ensnode/ensnode-sdk/internal"; + +const DEFAULT_LOG_LEVEL: LogLevel = "info"; + +export default pino({ + level: getLogLevelFromEnv(process.env, DEFAULT_LOG_LEVEL), + transport: + process.env.NODE_ENV === "production" + ? undefined + : { + target: "pino-pretty", + options: { + colorize: true, + ignore: "pid,hostname", + }, + }, +}); diff --git a/apps/ensapi/src/lib/resolution/forward-resolution.ts b/apps/ensapi/src/lib/resolution/forward-resolution.ts index 18fad49796..afd9742b20 100644 --- a/apps/ensapi/src/lib/resolution/forward-resolution.ts +++ b/apps/ensapi/src/lib/resolution/forward-resolution.ts @@ -19,6 +19,7 @@ import { TraceableENSProtocol, } from "@ensnode/ensnode-sdk"; +import logger from "@/lib/logger"; import { ENS_ROOT_REGISTRY } from "@/lib/protocol-acceleration/ens-root-registry"; import { findResolver } from "@/lib/protocol-acceleration/find-resolver"; import { getENSIP19ReverseNameRecordFromIndex } from "@/lib/protocol-acceleration/get-primary-name-from-index"; @@ -231,7 +232,7 @@ async function _resolveForward( // the selection should just be `{ name: true }`, but technically not prohibited to // select more records than just 'name', so just warn if that happens. if (selection.addresses !== undefined || selection.texts !== undefined) { - console.warn( + logger.warn( `Sanity Check(ENSIP-19 Reverse Resolvers Protocol Acceleration): expected a selection of exactly '{ name: true }' but received ${JSON.stringify(selection)}.`, ); } diff --git a/apps/ensapi/src/middleware/can-accelerate.middleware.ts b/apps/ensapi/src/middleware/can-accelerate.middleware.ts index 8ba1719b16..993cfdd4b9 100644 --- a/apps/ensapi/src/middleware/can-accelerate.middleware.ts +++ b/apps/ensapi/src/middleware/can-accelerate.middleware.ts @@ -13,6 +13,7 @@ import { } from "@ensnode/ensnode-sdk"; import { factory } from "@/lib/hono-factory"; +import logger from "@/lib/logger"; export type CanAccelerateVariables = { canAccelerate: boolean }; @@ -62,7 +63,7 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) // log one warning to the console if !hasProtocolAccelerationPlugin if (!didWarnNoProtocolAccelerationPlugin && !hasProtocolAccelerationPlugin) { - console.warn( + logger.warn( `ENSApi is connected to an ENSIndexer that does NOT include the ${PluginName.ProtocolAcceleration} plugin: ENSApi will NOT be able to accelerate Resolution API requests, even if ?accelerate=true. Resolution requests will abide by the full Forward/Reverse Resolution specification, including RPC calls and CCIP-Read requests to external CCIP-Read Gateways.`, ); @@ -81,7 +82,7 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) (!didInitialIndexingStatus && indexingStatusOk) || // first time (didInitialIndexingStatus && !prevIndexingStatusOk && indexingStatusOk) // future change in status ) { - console.log(`ENSIndexer Indexing Status: AVAILABLE`); + logger.info(`ENSIndexer Indexing Status: AVAILABLE`); } // log notice with reason when Indexing Status is unavilable @@ -90,11 +91,11 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) (didInitialIndexingStatus && prevIndexingStatusOk && !indexingStatusOk) // future change in status ) { if (c.var.indexingStatus.isRejected) { - console.warn( + logger.warn( `ENSIndexer Indexing Status: UNAVAILABLE. ENSApi was unable to fetch the current ENSIndexer Indexing Status: ${c.var.indexingStatus.reason}`, ); } else if (c.var.indexingStatus.value.responseCode === IndexingStatusResponseCodes.Error) { - console.warn( + logger.warn( `ENSIndexer Indexing Status: UNAVAILABLE. ENSIndexer is reporting an Indexing Status Error.`, ); } @@ -126,7 +127,7 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) (!didInitialRealtime && isWithinMaxRealtime) || // first time (didInitialRealtime && !prevIsWithinMaxRealtime && isWithinMaxRealtime) // future change in status ) { - console.log(`ENSIndexer is realtime, Protocol Acceleration is now ENABLED.`); + logger.info(`ENSIndexer is realtime, Protocol Acceleration is now ENABLED.`); } // log notice when ENSIndexer transitions out of realtime @@ -134,7 +135,7 @@ export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) (!didInitialRealtime && !isWithinMaxRealtime) || // first time (didInitialRealtime && prevIsWithinMaxRealtime && !isWithinMaxRealtime) // future change in status ) { - console.warn( + logger.warn( `ENSIndexer is NOT realtime (Worst Case Lag: ${c.var.indexingStatus.value.realtimeProjection.worstCaseDistance} seconds > ${MAX_REALTIME_DISTANCE_TO_ACCELERATE} seconds), Protocol Acceleration is currently DISABLED.`, ); } diff --git a/apps/ensrainbow/package.json b/apps/ensrainbow/package.json index 6e0b031662..01ad96eb95 100644 --- a/apps/ensrainbow/package.json +++ b/apps/ensrainbow/package.json @@ -35,7 +35,6 @@ "classic-level": "^1.4.1", "hono": "catalog:", "pino": "^10.1.0", - "pino-pretty": "^13.1.2", "progress": "^2.0.3", "protobufjs": "^7.4.0", "viem": "catalog:", @@ -46,6 +45,7 @@ "@types/node": "^20.17.14", "@types/progress": "^2.0.7", "@types/yargs": "^17.0.32", + "pino-pretty": "^13.1.2", "tsx": "^4.19.3", "typescript": "^5.3.3", "vitest": "catalog:" diff --git a/apps/ensrainbow/src/commands/convert-command.ts b/apps/ensrainbow/src/commands/convert-command.ts index dde51af7fa..e48258e6a5 100644 --- a/apps/ensrainbow/src/commands/convert-command.ts +++ b/apps/ensrainbow/src/commands/convert-command.ts @@ -50,6 +50,10 @@ function setupProgressBar(): ProgressBar { incomplete: " ", width: 40, total: 150000000, // estimated + stream: + logger.level === "silent" || logger.level === "fatal" + ? createWriteStream("/dev/null") + : undefined, }, ); } diff --git a/apps/ensrainbow/src/commands/ingest-protobuf-command.ts b/apps/ensrainbow/src/commands/ingest-protobuf-command.ts index 2f3b07277f..e11e69d3f7 100644 --- a/apps/ensrainbow/src/commands/ingest-protobuf-command.ts +++ b/apps/ensrainbow/src/commands/ingest-protobuf-command.ts @@ -1,4 +1,4 @@ -import { createReadStream } from "node:fs"; +import { createReadStream, createWriteStream } from "node:fs"; import ProgressBar from "progress"; import protobuf from "protobufjs"; @@ -105,6 +105,10 @@ export async function ingestProtobufCommand(options: IngestProtobufCommandOption incomplete: " ", width: 40, total: 1000000000, // Placeholder total + stream: + logger.level === "silent" || logger.level === "fatal" + ? createWriteStream("/dev/null") + : undefined, }, ); diff --git a/apps/ensrainbow/src/utils/logger.test.ts b/apps/ensrainbow/src/utils/logger.test.ts deleted file mode 100644 index 9bceb266fa..0000000000 --- a/apps/ensrainbow/src/utils/logger.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; - -import { - createLogger, - DEFAULT_LOG_LEVEL, - getEnvLogLevel, - parseLogLevel, - VALID_LOG_LEVELS, -} from "./logger"; - -describe("logger", () => { - describe("parseLogLevel", () => { - it("should accept valid log levels", () => { - VALID_LOG_LEVELS.forEach((level) => { - expect(parseLogLevel(level)).toBe(level); - }); - }); - - it("should handle case-insensitive input", () => { - expect(parseLogLevel("INFO")).toBe("info"); - expect(parseLogLevel("Debug")).toBe("debug"); - expect(parseLogLevel("ERROR")).toBe("error"); - }); - - it("should throw error for invalid log level", () => { - expect(() => parseLogLevel("invalid")).toThrow( - 'Invalid log level "invalid". Valid levels are: fatal, error, warn, info, debug, trace, silent', - ); - }); - }); - - describe("getEnvLogLevel", () => { - afterEach(() => { - vi.unstubAllEnvs(); - }); - - it("should return DEFAULT_LOG_LEVEL when LOG_LEVEL is not set", () => { - vi.stubEnv("LOG_LEVEL", undefined); - expect(getEnvLogLevel()).toBe(DEFAULT_LOG_LEVEL); - }); - - it("should return valid log level from environment", () => { - vi.stubEnv("LOG_LEVEL", "debug"); - expect(getEnvLogLevel()).toBe("debug"); - }); - - it("should error when invalid log level in environment", () => { - vi.stubEnv("LOG_LEVEL", "invalid"); - expect(() => getEnvLogLevel()).toThrow( - 'Environment variable error: (LOG_LEVEL): Invalid log level "invalid". Valid levels are: fatal, error, warn, info, debug, trace, silent.', - ); - }); - }); - - describe("createLogger", () => { - it("should create logger with default level when no level provided", () => { - const logger = createLogger(); - expect(logger.level).toBe(DEFAULT_LOG_LEVEL); - }); - - it("should create logger with specified level", () => { - const logger = createLogger("debug"); - expect(logger.level).toBe("debug"); - }); - }); -}); diff --git a/apps/ensrainbow/src/utils/logger.ts b/apps/ensrainbow/src/utils/logger.ts index 21e7a0f17e..b7e6f3d1a4 100644 --- a/apps/ensrainbow/src/utils/logger.ts +++ b/apps/ensrainbow/src/utils/logger.ts @@ -1,72 +1,20 @@ -import pino, { type LevelWithSilent } from "pino"; +import pino from "pino"; -import { getErrorMessage } from "@/utils/error-utils"; +import { getLogLevelFromEnv, type LogLevel } from "@ensnode/ensnode-sdk/internal"; -export type LogLevel = LevelWithSilent; +const DEFAULT_LOG_LEVEL: LogLevel = "info"; -export const DEFAULT_LOG_LEVEL: LogLevel = "info"; - -// Creating our own definition of the log levels recognized by Pino -// to provide a better user experience with clear error messages when invalid log levels are -// parsed. -export const VALID_LOG_LEVELS: LogLevel[] = [ - "fatal", - "error", - "warn", - "info", - "debug", - "trace", - "silent", -]; - -export function parseLogLevel(maybeLevel: string): LogLevel { - const normalizedLevel = maybeLevel.toLowerCase(); - if (VALID_LOG_LEVELS.includes(normalizedLevel as LogLevel)) { - return normalizedLevel as LogLevel; - } - throw new Error( - `Invalid log level "${maybeLevel}". Valid levels are: ${VALID_LOG_LEVELS.join(", ")}.`, - ); -} - -export function getEnvLogLevel(): LogLevel { - const envLogLevel = process.env.LOG_LEVEL; - if (!envLogLevel) { - return DEFAULT_LOG_LEVEL; - } - - try { - return parseLogLevel(envLogLevel); - } catch (error: unknown) { - const errorMessage = `Environment variable error: (LOG_LEVEL): ${getErrorMessage(error)}`; - // Log error to console since we can't use logger yet - console.error(errorMessage); - throw new Error(errorMessage); - } -} - -export function createLogger(level: LogLevel = DEFAULT_LOG_LEVEL): pino.Logger { - const isProduction = process.env.NODE_ENV === "production"; - - return pino({ - level, - ...(isProduction - ? {} // In production, use default pino output format +// Create and export the global logger instance +export const logger = pino({ + level: getLogLevelFromEnv(process.env, DEFAULT_LOG_LEVEL), + transport: + process.env.NODE_ENV === "production" + ? undefined : { - transport: { - target: "pino-pretty", - options: { - colorize: true, - translateTime: "HH:MM:ss", - ignore: "pid,hostname", - }, + target: "pino-pretty", + options: { + colorize: true, + ignore: "pid,hostname", }, - }), - }); -} - -// Create and export the global logger instance -export const logger = createLogger(getEnvLogLevel()); - -// Re-export pino types for convenience -export type { Logger } from "pino"; + }, +}); diff --git a/apps/ensrainbow/tsconfig.json b/apps/ensrainbow/tsconfig.json index f16245724b..3a25d8b4ba 100644 --- a/apps/ensrainbow/tsconfig.json +++ b/apps/ensrainbow/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "@ensnode/shared-configs/tsconfig.lib.json", "compilerOptions": { + "target": "esnext", + "typeRoots": ["./types"], "paths": { "@/*": ["./src/*"] } diff --git a/apps/ensrainbow/types/env.d.ts b/apps/ensrainbow/types/env.d.ts new file mode 100644 index 0000000000..dd956c6006 --- /dev/null +++ b/apps/ensrainbow/types/env.d.ts @@ -0,0 +1,7 @@ +import type { LogLevelEnvironment } from "@ensnode/ensnode-sdk/internal"; + +declare global { + namespace NodeJS { + interface ProcessEnv extends LogLevelEnvironment {} + } +} diff --git a/packages/ensnode-react/src/context.ts b/packages/ensnode-react/src/context.ts index db97bad131..42474d2430 100644 --- a/packages/ensnode-react/src/context.ts +++ b/packages/ensnode-react/src/context.ts @@ -1,11 +1,11 @@ import { createContext } from "react"; -import type { ENSNodeConfig } from "./types"; +import type { ENSNodeSDKConfig } from "./types"; /** * React context for ENSNode configuration */ -export const ENSNodeContext = createContext(undefined); +export const ENSNodeContext = createContext(undefined); /** * Display name for debugging diff --git a/packages/ensnode-react/src/hooks/index.ts b/packages/ensnode-react/src/hooks/index.ts index 49b99706c6..0d59970704 100644 --- a/packages/ensnode-react/src/hooks/index.ts +++ b/packages/ensnode-react/src/hooks/index.ts @@ -1,5 +1,5 @@ -export * from "./useENSIndexerConfig"; export * from "./useENSNodeConfig"; +export * from "./useENSNodeSDKConfig"; export * from "./useIndexingStatus"; export * from "./usePrimaryName"; export * from "./usePrimaryNames"; diff --git a/packages/ensnode-react/src/hooks/useENSIndexerConfig.ts b/packages/ensnode-react/src/hooks/useENSIndexerConfig.ts deleted file mode 100644 index b44782447e..0000000000 --- a/packages/ensnode-react/src/hooks/useENSIndexerConfig.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import type { ConfigResponse } from "@ensnode/ensnode-sdk"; - -import type { ConfigParameter, QueryParameter } from "../types"; -import { ASSUME_IMMUTABLE_QUERY, createENSIndexerConfigQueryOptions } from "../utils/query"; -import { useENSNodeConfig } from "./useENSNodeConfig"; - -type UseENSIndexerConfigParameters = QueryParameter; - -export function useENSIndexerConfig( - parameters: ConfigParameter & UseENSIndexerConfigParameters = {}, -) { - const { config, query = {} } = parameters; - const _config = useENSNodeConfig(config); - - const queryOptions = createENSIndexerConfigQueryOptions(_config); - - const options = { - ...queryOptions, - ...ASSUME_IMMUTABLE_QUERY, - ...query, - enabled: query.enabled ?? queryOptions.enabled, - }; - - return useQuery(options); -} diff --git a/packages/ensnode-react/src/hooks/useENSNodeConfig.ts b/packages/ensnode-react/src/hooks/useENSNodeConfig.ts index e1d3c297fa..c246d61afd 100644 --- a/packages/ensnode-react/src/hooks/useENSNodeConfig.ts +++ b/packages/ensnode-react/src/hooks/useENSNodeConfig.ts @@ -1,30 +1,27 @@ -"use client"; +import { useQuery } from "@tanstack/react-query"; -import { useContext } from "react"; +import type { ConfigResponse } from "@ensnode/ensnode-sdk"; -import { ENSNodeContext } from "../context"; -import type { ENSNodeConfig } from "../types"; +import type { QueryParameter, WithSDKConfigParameter } from "../types"; +import { ASSUME_IMMUTABLE_QUERY, createConfigQueryOptions } from "../utils/query"; +import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; -/** - * Hook to access the ENSNode configuration from context or parameters - * - * @param parameters - Optional config parameter that overrides context - * @returns The ENSNode configuration - * @throws Error if no config is available in context or parameters - */ -export function useENSNodeConfig( - config: TConfig | undefined, -): TConfig { - const contextConfig = useContext(ENSNodeContext); +type UseENSNodeConfigParameters = QueryParameter; - // Use provided config or fall back to context - const resolvedConfig = config ?? contextConfig; +export function useENSNodeConfig( + parameters: WithSDKConfigParameter & UseENSNodeConfigParameters = {}, +) { + const { config, query = {} } = parameters; + const _config = useENSNodeSDKConfig(config); - if (!resolvedConfig) { - throw new Error( - "useENSNodeConfig must be used within an ENSNodeProvider or you must pass a config parameter", - ); - } + const queryOptions = createConfigQueryOptions(_config); - return resolvedConfig as TConfig; + const options = { + ...queryOptions, + ...ASSUME_IMMUTABLE_QUERY, + ...query, + enabled: query.enabled ?? queryOptions.enabled, + }; + + return useQuery(options); } diff --git a/packages/ensnode-react/src/hooks/useENSNodeSDKConfig.ts b/packages/ensnode-react/src/hooks/useENSNodeSDKConfig.ts new file mode 100644 index 0000000000..2f7b1016aa --- /dev/null +++ b/packages/ensnode-react/src/hooks/useENSNodeSDKConfig.ts @@ -0,0 +1,30 @@ +"use client"; + +import { useContext } from "react"; + +import { ENSNodeContext } from "../context"; +import type { ENSNodeSDKConfig } from "../types"; + +/** + * Hook to access the ENSNodeSDKConfig from context or parameters. + * + * @param parameters - Optional config parameter that overrides context + * @returns The ENSNode configuration + * @throws Error if no config is available in context or parameters + */ +export function useENSNodeSDKConfig( + config: TConfig | undefined, +): TConfig { + const contextConfig = useContext(ENSNodeContext); + + // Use provided config or fall back to context + const resolvedConfig = config ?? contextConfig; + + if (!resolvedConfig) { + throw new Error( + "useENSNodeSDKConfig must be used within an ENSNodeProvider or you must pass a config parameter", + ); + } + + return resolvedConfig as TConfig; +} diff --git a/packages/ensnode-react/src/hooks/useIndexingStatus.ts b/packages/ensnode-react/src/hooks/useIndexingStatus.ts index d587a434ad..c56f3d6928 100644 --- a/packages/ensnode-react/src/hooks/useIndexingStatus.ts +++ b/packages/ensnode-react/src/hooks/useIndexingStatus.ts @@ -2,17 +2,19 @@ import { useQuery } from "@tanstack/react-query"; import type { IndexingStatusRequest, IndexingStatusResponse } from "@ensnode/ensnode-sdk"; -import type { ConfigParameter, QueryParameter } from "../types"; +import type { QueryParameter, WithSDKConfigParameter } from "../types"; import { createIndexingStatusQueryOptions } from "../utils/query"; -import { useENSNodeConfig } from "./useENSNodeConfig"; +import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; interface UseIndexingStatusParameters extends IndexingStatusRequest, QueryParameter {} -export function useIndexingStatus(parameters: ConfigParameter & UseIndexingStatusParameters = {}) { +export function useIndexingStatus( + parameters: WithSDKConfigParameter & UseIndexingStatusParameters = {}, +) { const { config, query = {} } = parameters; - const _config = useENSNodeConfig(config); + const _config = useENSNodeSDKConfig(config); const queryOptions = createIndexingStatusQueryOptions(_config); diff --git a/packages/ensnode-react/src/hooks/usePrimaryName.ts b/packages/ensnode-react/src/hooks/usePrimaryName.ts index b823c357fd..2bd7c9ffba 100644 --- a/packages/ensnode-react/src/hooks/usePrimaryName.ts +++ b/packages/ensnode-react/src/hooks/usePrimaryName.ts @@ -2,9 +2,9 @@ import { useQuery } from "@tanstack/react-query"; -import type { ConfigParameter, UsePrimaryNameParameters } from "../types"; +import type { UsePrimaryNameParameters, WithSDKConfigParameter } from "../types"; import { createPrimaryNameQueryOptions } from "../utils/query"; -import { useENSNodeConfig } from "./useENSNodeConfig"; +import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; /** * Resolves the primary name of a specified address (Reverse Resolution). @@ -37,9 +37,9 @@ import { useENSNodeConfig } from "./useENSNodeConfig"; * } * ``` */ -export function usePrimaryName(parameters: UsePrimaryNameParameters & ConfigParameter) { +export function usePrimaryName(parameters: UsePrimaryNameParameters & WithSDKConfigParameter) { const { config, query = {}, address, ...args } = parameters; - const _config = useENSNodeConfig(config); + const _config = useENSNodeSDKConfig(config); const canEnable = address !== null; diff --git a/packages/ensnode-react/src/hooks/usePrimaryNames.ts b/packages/ensnode-react/src/hooks/usePrimaryNames.ts index 0d02d53c5f..4c3f9026e3 100644 --- a/packages/ensnode-react/src/hooks/usePrimaryNames.ts +++ b/packages/ensnode-react/src/hooks/usePrimaryNames.ts @@ -2,9 +2,9 @@ import { useQuery } from "@tanstack/react-query"; -import type { ConfigParameter, UsePrimaryNamesParameters } from "../types"; +import type { UsePrimaryNamesParameters, WithSDKConfigParameter } from "../types"; import { createPrimaryNamesQueryOptions } from "../utils/query"; -import { useENSNodeConfig } from "./useENSNodeConfig"; +import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; /** * Resolves the primary names of a specified address across multiple chains. @@ -40,9 +40,9 @@ import { useENSNodeConfig } from "./useENSNodeConfig"; * } * ``` */ -export function usePrimaryNames(parameters: UsePrimaryNamesParameters & ConfigParameter) { +export function usePrimaryNames(parameters: UsePrimaryNamesParameters & WithSDKConfigParameter) { const { config, query = {}, address, ...args } = parameters; - const _config = useENSNodeConfig(config); + const _config = useENSNodeSDKConfig(config); const canEnable = address !== null; diff --git a/packages/ensnode-react/src/hooks/useRecords.ts b/packages/ensnode-react/src/hooks/useRecords.ts index e3ef4c0ea8..ee7d9ed9f9 100644 --- a/packages/ensnode-react/src/hooks/useRecords.ts +++ b/packages/ensnode-react/src/hooks/useRecords.ts @@ -4,9 +4,9 @@ import { useQuery } from "@tanstack/react-query"; import type { ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; -import type { ConfigParameter, UseRecordsParameters } from "../types"; +import type { UseRecordsParameters, WithSDKConfigParameter } from "../types"; import { createRecordsQueryOptions } from "../utils/query"; -import { useENSNodeConfig } from "./useENSNodeConfig"; +import { useENSNodeSDKConfig } from "./useENSNodeSDKConfig"; /** * Resolves records for an ENS name (Forward Resolution). @@ -51,10 +51,10 @@ import { useENSNodeConfig } from "./useENSNodeConfig"; * ``` */ export function useRecords( - parameters: UseRecordsParameters & ConfigParameter, + parameters: UseRecordsParameters & WithSDKConfigParameter, ) { const { config, query = {}, name, ...args } = parameters; - const _config = useENSNodeConfig(config); + const _config = useENSNodeSDKConfig(config); const canEnable = name !== null; diff --git a/packages/ensnode-react/src/provider.tsx b/packages/ensnode-react/src/provider.tsx index 54f27789cc..457db1319e 100644 --- a/packages/ensnode-react/src/provider.tsx +++ b/packages/ensnode-react/src/provider.tsx @@ -7,11 +7,11 @@ import { createElement, useMemo } from "react"; import { ENSNodeClient } from "@ensnode/ensnode-sdk"; import { ENSNodeContext } from "./context"; -import type { ENSNodeConfig } from "./types"; +import type { ENSNodeSDKConfig } from "./types"; export interface ENSNodeProviderProps { /** ENSNode configuration */ - config: ENSNodeConfig; + config: ENSNodeSDKConfig; /** * Optional QueryClient instance. If provided, you must wrap your app with QueryClientProvider yourself. @@ -31,7 +31,7 @@ function ENSNodeInternalProvider({ config, }: { children?: React.ReactNode; - config: ENSNodeConfig; + config: ENSNodeSDKConfig; }) { // Memoize the config to prevent unnecessary re-renders const memoizedConfig = useMemo(() => config, [config]); @@ -94,7 +94,7 @@ export function ENSNodeProvider(parameters: React.PropsWithChildren { /** * Configuration parameter for hooks that need access to config */ -export interface ConfigParameter { +export interface WithSDKConfigParameter { config?: TConfig | undefined; } diff --git a/packages/ensnode-react/src/utils/query.ts b/packages/ensnode-react/src/utils/query.ts index 0762e467eb..6e77b25df6 100644 --- a/packages/ensnode-react/src/utils/query.ts +++ b/packages/ensnode-react/src/utils/query.ts @@ -10,7 +10,7 @@ import { type ResolverRecordsSelection, } from "@ensnode/ensnode-sdk"; -import type { ENSNodeConfig } from "../types"; +import type { ENSNodeSDKConfig } from "../types"; /** * Immutable query options for data that is assumed to be immutable and should only be fetched once per full page refresh per unique key. @@ -61,7 +61,7 @@ export const queryKeys = { * Creates query options for Records Resolution */ export function createRecordsQueryOptions( - config: ENSNodeConfig, + config: ENSNodeSDKConfig, args: ResolveRecordsRequest, ) { return { @@ -78,7 +78,7 @@ export function createRecordsQueryOptions { // arrange const requestUrl = new URL(`/api/config`, DEFAULT_ENSNODE_API_URL); const serializedMockedResponse = EXAMPLE_CONFIG_RESPONSE; - const mockedResponse = deserializeENSIndexerPublicConfig(serializedMockedResponse); + const mockedResponse = deserializeENSApiPublicConfig(serializedMockedResponse); const client = new ENSNodeClient(); mockFetch.mockResolvedValueOnce({ diff --git a/packages/ensnode-sdk/src/client.ts b/packages/ensnode-sdk/src/client.ts index 2286cd2e5c..ac9b6d4037 100644 --- a/packages/ensnode-sdk/src/client.ts +++ b/packages/ensnode-sdk/src/client.ts @@ -15,10 +15,7 @@ import type { ResolveRecordsResponse, } from "./api/types"; import { ClientError } from "./client-error"; -import { - deserializeENSIndexerPublicConfig, - type SerializedENSIndexerPublicConfig, -} from "./ensindexer"; +import { deserializeENSApiPublicConfig, type SerializedENSApiPublicConfig } from "./ensapi"; import type { ResolverRecordsSelection } from "./resolution"; /** @@ -289,10 +286,9 @@ export class ENSNodeClient { const response = await fetch(url); - let responseData: unknown; - // ENSNode API should always allow parsing a response as JSON object. // If for some reason it's not the case, throw an error. + let responseData: unknown; try { responseData = await response.json(); } catch { @@ -304,7 +300,7 @@ export class ENSNodeClient { throw new Error(`Fetching ENSNode Config Failed: ${errorResponse.message}`); } - return deserializeENSIndexerPublicConfig(responseData as SerializedENSIndexerPublicConfig); + return deserializeENSApiPublicConfig(responseData as SerializedENSApiPublicConfig); } /** @@ -321,10 +317,9 @@ export class ENSNodeClient { const response = await fetch(url); - let responseData: unknown; - // ENSNode API should always allow parsing a response as JSON object. // If for some reason it's not the case, throw an error. + let responseData: unknown; try { responseData = await response.json(); } catch { @@ -333,9 +328,8 @@ export class ENSNodeClient { // handle response errors accordingly if (!response.ok) { - let errorResponse: ErrorResponse | undefined; - // check for a generic errorResponse + let errorResponse: ErrorResponse | undefined; try { errorResponse = deserializeErrorResponse(responseData); } catch { diff --git a/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts b/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts new file mode 100644 index 0000000000..3fdc2970a2 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/conversions.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; + +import { ENSNamespaceIds } from "@ensnode/datasources"; + +import { PluginName } from "../../ensindexer"; +import { deserializeENSApiPublicConfig, serializeENSApiPublicConfig } from "."; +import type { ENSApiPublicConfig } from "./types"; + +const MOCK_ENSAPI_PUBLIC_CONFIG = { + version: "0.36.0", + ensIndexerPublicConfig: { + namespace: ENSNamespaceIds.Mainnet, + databaseSchemaName: "ensapi", + indexedChainIds: new Set([1]), + isSubgraphCompatible: false, + labelSet: { labelSetId: "subgraph", labelSetVersion: 0 }, + plugins: [PluginName.Subgraph], + versionInfo: { + ensDb: "0.36.0", + ensIndexer: "0.36.0", + ensRainbow: "0.36.0", + ensRainbowSchema: 1, + ensNormalize: "1.1.1", + nodejs: "20.0.0", + ponder: "0.5.0", + }, + }, +} satisfies ENSApiPublicConfig; + +const MOCK_SERIALIZED_ENSAPI_PUBLIC_CONFIG = serializeENSApiPublicConfig(MOCK_ENSAPI_PUBLIC_CONFIG); + +describe("ENSApi Config Serialization/Deserialization", () => { + describe("serializeENSApiPublicConfig", () => { + it("serializes ENSAPI public config correctly", () => { + const result = serializeENSApiPublicConfig(MOCK_ENSAPI_PUBLIC_CONFIG); + + expect(result).toEqual({ + version: "0.36.0", + ensIndexerPublicConfig: { + namespace: ENSNamespaceIds.Mainnet, + databaseSchemaName: "ensapi", + indexedChainIds: [1], + isSubgraphCompatible: false, + labelSet: { labelSetId: "subgraph", labelSetVersion: 0 }, + plugins: [PluginName.Subgraph], + versionInfo: { + ensDb: "0.36.0", + ensIndexer: "0.36.0", + ensRainbow: "0.36.0", + ensRainbowSchema: 1, + ensNormalize: "1.1.1", + nodejs: "20.0.0", + ponder: "0.5.0", + }, + }, + }); + }); + }); + + describe("deserializeENSApiPublicConfig", () => { + it("deserializes ENSAPI public config correctly", () => { + const serialized = serializeENSApiPublicConfig(MOCK_ENSAPI_PUBLIC_CONFIG); + const result = deserializeENSApiPublicConfig(serialized); + + expect(result).toEqual(MOCK_ENSAPI_PUBLIC_CONFIG); + }); + + it("handles validation errors with custom value label", () => { + const invalidConfig = { + ...MOCK_SERIALIZED_ENSAPI_PUBLIC_CONFIG, + version: "", // Invalid: empty string + }; + + expect(() => deserializeENSApiPublicConfig(invalidConfig, "testConfig")).toThrow( + /testConfig.version/, + ); + }); + }); + + describe("round-trip conversion", () => { + it("maintains data integrity through serialize -> deserialize cycle", () => { + const serialized = serializeENSApiPublicConfig(MOCK_ENSAPI_PUBLIC_CONFIG); + const deserialized = deserializeENSApiPublicConfig(serialized); + + expect(deserialized).toStrictEqual(MOCK_ENSAPI_PUBLIC_CONFIG); + }); + }); +}); diff --git a/packages/ensnode-sdk/src/ensapi/config/deserialize.ts b/packages/ensnode-sdk/src/ensapi/config/deserialize.ts new file mode 100644 index 0000000000..0daa541bd2 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/deserialize.ts @@ -0,0 +1,24 @@ +import { prettifyError, ZodError } from "zod/v4"; + +import type { SerializedENSApiPublicConfig } from "./serialized-types"; +import type { ENSApiPublicConfig } from "./types"; +import { makeENSApiPublicConfigSchema } from "./zod-schemas"; + +/** + * Deserialize a {@link ENSApiPublicConfig} object. + */ +export function deserializeENSApiPublicConfig( + maybeConfig: SerializedENSApiPublicConfig, + valueLabel?: string, +): ENSApiPublicConfig { + const schema = makeENSApiPublicConfigSchema(valueLabel); + try { + return schema.parse(maybeConfig); + } catch (error) { + if (error instanceof ZodError) { + throw new Error(`Cannot deserialize ENSApiPublicConfig:\n${prettifyError(error)}\n`); + } + + throw error; + } +} diff --git a/packages/ensnode-sdk/src/ensapi/config/index.ts b/packages/ensnode-sdk/src/ensapi/config/index.ts new file mode 100644 index 0000000000..bff2897b57 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/index.ts @@ -0,0 +1,5 @@ +export * from "./deserialize"; +export * from "./serialize"; +export * from "./serialized-types"; +export * from "./types"; +export * from "./zod-schemas"; diff --git a/packages/ensnode-sdk/src/ensapi/config/serialize.ts b/packages/ensnode-sdk/src/ensapi/config/serialize.ts new file mode 100644 index 0000000000..4741e376b5 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/serialize.ts @@ -0,0 +1,17 @@ +import { serializeENSIndexerPublicConfig } from "../../ensindexer"; +import type { SerializedENSApiPublicConfig } from "./serialized-types"; +import type { ENSApiPublicConfig } from "./types"; + +/** + * Serialize a {@link ENSApiPublicConfig} object. + */ +export function serializeENSApiPublicConfig( + config: ENSApiPublicConfig, +): SerializedENSApiPublicConfig { + const { version, ensIndexerPublicConfig } = config; + + return { + version, + ensIndexerPublicConfig: serializeENSIndexerPublicConfig(ensIndexerPublicConfig), + } satisfies SerializedENSApiPublicConfig; +} diff --git a/packages/ensnode-sdk/src/ensapi/config/serialized-types.ts b/packages/ensnode-sdk/src/ensapi/config/serialized-types.ts new file mode 100644 index 0000000000..f437da884a --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/serialized-types.ts @@ -0,0 +1,13 @@ +import type { SerializedENSIndexerPublicConfig } from "../../ensindexer"; +import type { ENSApiPublicConfig } from "./types"; + +/** + * Serialized representation of {@link ENSApiPublicConfig} + */ +export interface SerializedENSApiPublicConfig + extends Omit { + /** + * Serialized representation of {@link ENSApiPublicConfig.ensIndexerPublicConfig}. + */ + ensIndexerPublicConfig: SerializedENSIndexerPublicConfig; +} diff --git a/packages/ensnode-sdk/src/ensapi/config/types.ts b/packages/ensnode-sdk/src/ensapi/config/types.ts new file mode 100644 index 0000000000..5af6bc59ab --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/types.ts @@ -0,0 +1,24 @@ +import type { ENSIndexerPublicConfig } from "../../ensindexer"; + +/** + * Complete public configuration object for ENSApi. + * + * Contains ENSApi-specific configuration at the top level and + * embeds the complete ENSIndexer public configuration. + */ +export interface ENSApiPublicConfig { + /** + * ENSApi service version + * + * @see https://ghcr.io/namehash/ensnode/ensapi + */ + version: string; + + /** + * Complete ENSIndexer public configuration + * + * Contains all ENSIndexer public configuration including + * namespace, plugins, version info, etc. + */ + ensIndexerPublicConfig: ENSIndexerPublicConfig; +} diff --git a/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts b/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts new file mode 100644 index 0000000000..f495c995d9 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts @@ -0,0 +1,17 @@ +import { z } from "zod/v4"; + +import { makeENSIndexerPublicConfigSchema } from "../../ensindexer/config/zod-schemas"; + +/** + * Create a Zod schema for validating a serialized ENSApiPublicConfig. + * + * @param valueLabel - Optional label for the value being validated (used in error messages) + */ +export function makeENSApiPublicConfigSchema(valueLabel?: string) { + const label = valueLabel ?? "ENSApiPublicConfig"; + + return z.strictObject({ + version: z.string().min(1, `${label}.version must be a non-empty string`), + ensIndexerPublicConfig: makeENSIndexerPublicConfigSchema(`${label}.ensIndexerPublicConfig`), + }); +} diff --git a/packages/ensnode-sdk/src/ensapi/index.ts b/packages/ensnode-sdk/src/ensapi/index.ts new file mode 100644 index 0000000000..5c62e04f5e --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/index.ts @@ -0,0 +1 @@ +export * from "./config"; diff --git a/packages/ensnode-sdk/src/index.ts b/packages/ensnode-sdk/src/index.ts index 32dfbeec60..32332cf0c7 100644 --- a/packages/ensnode-sdk/src/index.ts +++ b/packages/ensnode-sdk/src/index.ts @@ -6,6 +6,7 @@ export { } from "./client"; export * from "./client-error"; export * from "./ens"; +export * from "./ensapi"; export * from "./ensindexer"; export * from "./ensrainbow"; export * from "./resolution"; diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 31cff5a512..63e0c985b6 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -23,5 +23,6 @@ export * from "./shared/config/types"; export * from "./shared/config/validatons"; export * from "./shared/config/zod-schemas"; export * from "./shared/datasources-with-resolvers"; +export * from "./shared/log-level"; export * from "./shared/protocol-acceleration/interpret-record-values"; export * from "./shared/zod-schemas"; diff --git a/packages/ensnode-sdk/src/resolution/index.ts b/packages/ensnode-sdk/src/resolution/index.ts index 6e7390288b..9824550cd5 100644 --- a/packages/ensnode-sdk/src/resolution/index.ts +++ b/packages/ensnode-sdk/src/resolution/index.ts @@ -1,4 +1,3 @@ -export * from "./default-records-selection"; export * from "./ensip19-chainid"; export * from "./identity"; export * from "./resolver-records-response"; diff --git a/packages/ensnode-sdk/src/shared/config/environments.ts b/packages/ensnode-sdk/src/shared/config/environments.ts index c4b3796e68..e9bd00e70a 100644 --- a/packages/ensnode-sdk/src/shared/config/environments.ts +++ b/packages/ensnode-sdk/src/shared/config/environments.ts @@ -35,3 +35,10 @@ export interface PortEnvironment { * May contain a comma separated list of one or more URLs. */ export type ChainIdSpecificRpcEnvironmentVariable = string; + +/** + * Environment variables for log level configuration. + */ +export type LogLevelEnvironment = { + LOG_LEVEL?: string; +}; diff --git a/packages/ensnode-sdk/src/shared/log-level.test.ts b/packages/ensnode-sdk/src/shared/log-level.test.ts new file mode 100644 index 0000000000..58fa3516a7 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/log-level.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { LogLevelEnvironment } from "../internal"; +import { getLogLevelFromEnv } from "./log-level"; + +describe("logger", () => { + describe("getLogLevelFromEnv", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("should return default when LOG_LEVEL is not set", () => { + vi.stubEnv("LOG_LEVEL", undefined); + expect(getLogLevelFromEnv(process.env as LogLevelEnvironment, "debug")).toBe("debug"); + }); + + it("should return valid log level from environment", () => { + vi.stubEnv("LOG_LEVEL", "warn"); + expect(getLogLevelFromEnv(process.env as LogLevelEnvironment, "warn")).toBe("warn"); + }); + + it("should return default when invalid log level in environment", () => { + vi.stubEnv("LOG_LEVEL", "invalid"); + expect(getLogLevelFromEnv(process.env as LogLevelEnvironment, "debug")).toBe("debug"); + }); + }); +}); diff --git a/packages/ensnode-sdk/src/shared/log-level.ts b/packages/ensnode-sdk/src/shared/log-level.ts new file mode 100644 index 0000000000..e38c51b954 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/log-level.ts @@ -0,0 +1,21 @@ +import { z } from "zod/v4"; + +import type { LogLevelEnvironment } from "../internal"; + +/** + * Set of valid log levels, mirroring pino#LogLevelWithSilent. + */ +const LogLevelSchema = z.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]); + +export type LogLevel = z.infer; + +export function getLogLevelFromEnv(env: LogLevelEnvironment, defaultLogLevel: LogLevel): LogLevel { + try { + return LogLevelSchema.default(defaultLogLevel).parse(env.LOG_LEVEL); + } catch { + console.warn( + `Invalid LOG_LEVEL '${env.LOG_LEVEL}', expected one of '${Object.values(LogLevelSchema.enum).join("' | '")}' defaulting to '${defaultLogLevel}'`, + ); + return defaultLogLevel; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ac9d41fc0..3cbce62484 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -327,6 +327,9 @@ importers: p-retry: specifier: ^7.1.0 version: 7.1.0 + pino: + specifier: 10.1.0 + version: 10.1.0 ponder: specifier: 'catalog:' version: 0.13.14(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@22.15.3)(bufferutil@4.0.9)(hono@4.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.3)(typescript@5.7.3)(utf-8-validate@5.0.10)(viem@2.23.2(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(zod@3.25.76))(yaml@2.7.0)(zod@3.25.76) @@ -346,6 +349,9 @@ importers: '@types/node': specifier: 'catalog:' version: 22.15.3 + pino-pretty: + specifier: ^13.1.2 + version: 13.1.2 tsx: specifier: ^4.7.1 version: 4.19.3 @@ -437,9 +443,6 @@ importers: pino: specifier: 10.1.0 version: 10.1.0 - pino-pretty: - specifier: ^13.1.2 - version: 13.1.2 progress: specifier: ^2.0.3 version: 2.0.3 @@ -465,6 +468,9 @@ importers: '@types/yargs': specifier: ^17.0.32 version: 17.0.33 + pino-pretty: + specifier: ^13.1.2 + version: 13.1.2 tsx: specifier: ^4.19.3 version: 4.19.3 diff --git a/vitest.config.ts b/vitest.config.ts index d8873fd84b..8ff5bda545 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { projects: ["apps/*/vitest.config.ts", "packages/*/vitest.config.ts"], + // we place LOG_LEVEL here at the root such that running vitest within a specific project continues + // to print logs at the default log level env: { LOG_LEVEL: "silent", },