From 951fa4bf1aae3359ac8d208b4c33ac775ddd9bce Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 22 May 2026 09:54:19 -0500 Subject: [PATCH 01/13] feat(examples): link Domains by DomainId to preserve ENSv1/ENSv2 variant Split the enskit example's Domain browser into /domain/name/:name and /domain/id/:id, both backed by a single domain(by: DomainIdInput!) query. Search, Account, and subdomain/parent links now navigate by DomainId so clicking the v1 variant of a name lands on the v1 Domain rather than its v2 canonical. Closes #2141 Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/enskit-example-domain-by-id.md | 5 + .../enskit-react-example/src/AccountView.tsx | 10 +- examples/enskit-react-example/src/App.tsx | 7 +- .../enskit-react-example/src/DomainView.tsx | 129 ++++++++++++------ .../enskit-react-example/src/SearchView.tsx | 8 +- 5 files changed, 105 insertions(+), 54 deletions(-) create mode 100644 .changeset/enskit-example-domain-by-id.md diff --git a/.changeset/enskit-example-domain-by-id.md b/.changeset/enskit-example-domain-by-id.md new file mode 100644 index 0000000000..eba39b2c0b --- /dev/null +++ b/.changeset/enskit-example-domain-by-id.md @@ -0,0 +1,5 @@ +--- +"@ensnode/enskit-react-example": patch +--- + +Refine Domain links to distinguish ENSv1 vs ENSv2 variants. The Domain browser now has two routes — `/domain/name/:name` (resolves to a name's Canonical Domain) and `/domain/id/:id` (addresses an exact Domain) — both backed by a single `domain(by: DomainIdInput!)` query. Search, Account, and subdomain/parent links now navigate by `DomainId` so clicking the v1 variant of a name lands on the v1 Domain rather than its v2 canonical. diff --git a/examples/enskit-react-example/src/AccountView.tsx b/examples/enskit-react-example/src/AccountView.tsx index 895902d796..1f90dba705 100644 --- a/examples/enskit-react-example/src/AccountView.tsx +++ b/examples/enskit-react-example/src/AccountView.tsx @@ -63,13 +63,13 @@ function RenderAccount({ address }: { address: NormalizedAddress }) { {domains.edges.map((edge) => (
  • {/* - TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - {edge.node.canonical ? ( - - {beautifyInterpretedName(edge.node.canonical.name.interpreted)} + TODO: after upgrading v2-sepolia to have materialized canonical name, update the label to: + + {beautifyInterpretedName(edge.node.canonical.name.interpreted)} */} {edge.node.name ? ( - + // link by DomainId so the exact ENSv1/ENSv2 variant the user clicked is preserved + {beautifyInterpretedName(edge.node.name)} ) : ( diff --git a/examples/enskit-react-example/src/App.tsx b/examples/enskit-react-example/src/App.tsx index da4e26c1d4..a902c06260 100644 --- a/examples/enskit-react-example/src/App.tsx +++ b/examples/enskit-react-example/src/App.tsx @@ -5,7 +5,7 @@ import { StrictMode } from "react"; import { HashRouter, Link, Outlet, Route, Routes } from "react-router"; import { AccountView } from "./AccountView"; -import { DomainView } from "./DomainView"; +import { DomainByIdView, DomainByNameView } from "./DomainView"; import { RegistryView } from "./RegistryView"; import { SearchView } from "./SearchView"; @@ -26,7 +26,7 @@ function Layout() { return ( <> @@ -56,7 +56,8 @@ export function App() { }> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/examples/enskit-react-example/src/DomainView.tsx b/examples/enskit-react-example/src/DomainView.tsx index 3ea47cef1b..44ab27db65 100644 --- a/examples/enskit-react-example/src/DomainView.tsx +++ b/examples/enskit-react-example/src/DomainView.tsx @@ -1,6 +1,11 @@ import { EnsureInterpretedName } from "enskit/react"; import { type FragmentOf, graphql, readFragment, useOmnigraphQuery } from "enskit/react/omnigraph"; -import { asLiteralName, beautifyInterpretedName, type InterpretedName } from "enssdk"; +import { + asLiteralName, + beautifyInterpretedName, + type DomainId, + type InterpretedName, +} from "enssdk"; import { useState } from "react"; import { Link, Navigate, useParams } from "react-router"; @@ -15,14 +20,17 @@ const DomainFragment = graphql(` } `); -const DomainByNameQuery = graphql( +// A single query that identifies a Domain by either its DomainId or its Name. `Query.domain` accepts +// a `DomainIdInput` (a `@oneOf` of `{ id }` or `{ name }`), so both views below share this one query +// and simply pass whichever stable reference they have. +const DomainByQuery = graphql( ` - query DomainByName($name: InterpretedName!, $first: Int!, $after: String) { - domain(by: { name: $name }) { + query DomainBy($by: DomainIdInput!, $first: Int!, $after: String) { + domain(by: $by) { ...DomainFragment # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - # parent { canonical { name { interpreted } } } - parent { name } + # parent { id canonical { name { interpreted } } } + parent { id name } subdomains(first: $first, after: $after) { edges { node { @@ -40,20 +48,45 @@ const DomainByNameQuery = graphql( [DomainFragment], ); +// A stable reference to a Domain: either its DomainId or its Name. +// +// This is the "Stable IDs vs. Namegraph addressing" distinction from the Omnigraph docs: +// https://ensnode.io/docs/integrate/omnigraph#stable-ids-vs-namegraph-addressing +// +// Identifying by Name is *Namegraph addressing*: it resolves to whichever Domain the namespace +// currently considers Canonical for that name, so the target can change as canonicality changes (and +// a name like `vitalik.eth` may have both an ENSv1 and an ENSv2 Domain that disagree on which is +// canonical). Identifying by DomainId is a *stable reference*: it always addresses one exact Domain, +// preserving its ENSv1/ENSv2 variant. So whenever we already hold a DomainId (search results, owned +// domains, subdomains, a parent pointer), we link by `id` — that way clicking the v1 variant of a +// name lands the user on the v1 Domain instead of being silently redirected to its v2 canonical. +type DomainBy = { id: DomainId } | { name: InterpretedName }; + const SUBDOMAINS_PAGE_SIZE = 20; +// sepolia-v2's ENSv1Resolver is misconfigured, so ENSv1-only names aren't currently resolvable. +function SepoliaNotice() { + return ( +
    + Heads up! sepolia-v2's ENSv1Resolver is misconfigured, and ENSv1-only names aren't resolvable, + so they're not currently visible here! This will be fixed by the ENS Team in the near future. + If you followed a link to a Domain and it isn't showing up here, it's likely an ENSv1-only + name (unmigrated) and isn't currently resolvable. +
    + ); +} + function SubdomainLink({ data }: { data: FragmentOf }) { const domain = readFragment(DomainFragment, data); return (
  • {domain.name ? ( - {beautifyInterpretedName(domain.name)} - // TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - // {domain.canonical ? ( - // - // {beautifyInterpretedName(domain.canonical.name.interpreted)} - // + // link by DomainId so the exact Domain (and its ENSv1/ENSv2 variant) is preserved + {beautifyInterpretedName(domain.name)} ) : ( non-canonical domain )}{" "} @@ -69,30 +102,33 @@ function SubdomainLink({ data }: { data: FragmentOf }) { ); } -function RenderDomain({ name }: { name: InterpretedName }) { +function RenderDomain({ by }: { by: DomainBy }) { const [after, setAfter] = useState(null); const [result] = useOmnigraphQuery({ - query: DomainByNameQuery, - variables: { name, first: SUBDOMAINS_PAGE_SIZE, after }, + query: DomainByQuery, + variables: { by, first: SUBDOMAINS_PAGE_SIZE, after }, }); const { data, fetching, error } = result; if (!data && fetching) return

    Loading...

    ; if (error) return

    Error: {error.message}

    ; - if (!data?.domain) return

    No domain was found with name '{beautifyInterpretedName(name)}'.

    ; + if (!data?.domain) { + const reference = "id" in by ? `id '${by.id}'` : `name '${beautifyInterpretedName(by.name)}'`; + return

    No domain was found with {reference}.

    ; + } const domain = readFragment(DomainFragment, data.domain); const { subdomains } = data.domain; return (
    - {/* + {/* TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: -

    {beautifyInterpretedName(domain.canonical?.name.interpreted ?? name)}

    +

    {beautifyInterpretedName(domain.canonical?.name.interpreted ?? domain.id)}

    */} -

    {beautifyInterpretedName(domain.name ?? name)}

    +

    {domain.name ? beautifyInterpretedName(domain.name) : domain.id}

    Owner:{" "} {domain.owner ? ( @@ -105,16 +141,16 @@ function RenderDomain({ name }: { name: InterpretedName }) {

    Version: {domain.__typename}

    - {/* + {/* TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: {data.domain.parent?.canonical && ( - + ← {beautifyInterpretedName(data.domain.parent.canonical.name.interpreted)} - )} + )} */} {data.domain.parent?.name && ( - + ← {beautifyInterpretedName(data.domain.parent.name)} )} @@ -151,25 +187,19 @@ function RenderDomain({ name }: { name: InterpretedName }) { ); } -export function DomainView() { +// Identify a Domain by its Name (`/domain/name/:name`). Resolves to the name's Canonical Domain. +export function DomainByNameView() { const params = useParams(); - // if a user accesses '/domain' directly, redirect to '/domain/eth' + // if a user accesses '/domain/name' directly, redirect to '/domain/name/eth' // TODO: render the set of tlds - if (params.name === undefined || params.name === "") return ; + if (params.name === undefined || params.name === "") + return ; - // here we ensure that the provided /domain/:name parameter is an InterpretedName + // here we ensure that the provided /domain/name/:name parameter is an InterpretedName return ( <> -
    - Heads up! sepolia-v2's ENSv1Resolver is misconfigured, and ENSv1-only names aren't - resolvable, so they're not currently visible here! This will be fixed by the ENS Team in the - near future. If you followed a link to a Domain and it isn't showing up here, it's likely an - ENSv1-only name (unmigrated) and isn't currently resolvable. -
    + } + coerced={(name) => } // // this name can't conform to InterpretedName nor can it be coerced: it is malformed: show an error malformed={(name) => (

    Invalid name: '{name}'

    - Back to 'eth' Domain. + Back to 'eth' Domain.
    )} > - {(name) => } + {(name) => }
    ); } + +// Identify a Domain by its DomainId (`/domain/id/:id`). Addresses the exact Domain, preserving its +// ENSv1/ENSv2 variant. This is the preferred link target when a stable DomainId is already in hand. +export function DomainByIdView() { + const params = useParams(); + + if (params.id === undefined || params.id === "") + return ; + + // a DomainId is an opaque, stable identifier; it requires no normalization + const id = params.id as DomainId; + + return ( + <> + + + + ); +} diff --git a/examples/enskit-react-example/src/SearchView.tsx b/examples/enskit-react-example/src/SearchView.tsx index a3b64219c7..c4b65e9a14 100644 --- a/examples/enskit-react-example/src/SearchView.tsx +++ b/examples/enskit-react-example/src/SearchView.tsx @@ -94,13 +94,9 @@ export function SearchView() { return (
  • ({edge.node.__typename === "ENSv1Domain" ? "v1" : "v2"}){" "} - + {/* link by DomainId so the exact ENSv1/ENSv2 variant the user clicked is preserved */} + {beautifyInterpretedName(edge.node.name)} - {/* - TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - - {beautifyInterpretedName(edge.node.canonical.name.interpreted)} - */}
  • ); From 8c672d4ece0ffd5340ab7086cb1bbd85b09936cc Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 22 May 2026 09:56:00 -0500 Subject: [PATCH 02/13] chore: drop changeset for example app Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/enskit-example-domain-by-id.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/enskit-example-domain-by-id.md diff --git a/.changeset/enskit-example-domain-by-id.md b/.changeset/enskit-example-domain-by-id.md deleted file mode 100644 index eba39b2c0b..0000000000 --- a/.changeset/enskit-example-domain-by-id.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@ensnode/enskit-react-example": patch ---- - -Refine Domain links to distinguish ENSv1 vs ENSv2 variants. The Domain browser now has two routes — `/domain/name/:name` (resolves to a name's Canonical Domain) and `/domain/id/:id` (addresses an exact Domain) — both backed by a single `domain(by: DomainIdInput!)` query. Search, Account, and subdomain/parent links now navigate by `DomainId` so clicking the v1 variant of a name lands on the v1 Domain rather than its v2 canonical. From 758ed7700c7e7e128c221a2d473d5b228f5d4bb4 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 22 May 2026 10:09:10 -0500 Subject: [PATCH 03/13] chore(examples): unpin to workspace:* and migrate to 1.14.x Omnigraph schema The example apps pinned published preview enssdk/enskit, masking them from workspace schema changes. Switch all examples to workspace:* and migrate their queries from the removed Domain.name field to the materialized canonical { name { beautified } }. Point hosted-instance usage at the `blue` v2-sepolia deployment (ENSNode 1.14.x), which serves the matching schema. Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/enskit-react-example/README.md | 4 +- examples/enskit-react-example/package.json | 4 +- .../enskit-react-example/src/AccountView.tsx | 20 ++------ examples/enskit-react-example/src/App.tsx | 5 +- .../enskit-react-example/src/DomainView.tsx | 29 +++--------- .../enskit-react-example/src/SearchView.tsx | 9 ++-- examples/enssdk-example/README.md | 4 +- examples/enssdk-example/package.json | 2 +- examples/enssdk-example/src/index.ts | 13 ++--- examples/omnigraph-graphql-example/README.md | 4 +- .../omnigraph-graphql-example/src/index.ts | 17 ++----- pnpm-lock.yaml | 47 +++---------------- 12 files changed, 42 insertions(+), 116 deletions(-) diff --git a/examples/enskit-react-example/README.md b/examples/enskit-react-example/README.md index 894c6b5efc..e54f8bc27f 100644 --- a/examples/enskit-react-example/README.md +++ b/examples/enskit-react-example/README.md @@ -8,14 +8,14 @@ This app is hosted at [https://enskit-react-example.ensnode.io/](https://enskit- ## Usage (with NameHash Hosted Instance) -> **Version compatibility:** Our hosted ENSNode instances currently run ENSNode v1.13. If you are querying them from your own app, you **must** use `enskit@1.13.1` and `enssdk@1.13.1`. The latest published versions (`1.14.0+`) contain breaking changes in the Omnigraph API data model not yet deployed to our hosted infrastructure. This notice will be removed once the hosted instances are upgraded. +> **Schema version:** This example tracks the latest Omnigraph schema (ENSNode 1.14.x) via `workspace:*` `enskit`/`enssdk`. It connects to the `blue` hosted deployment, which runs 1.14.x; the default (non-`blue`) hosted instances still serve an older schema that wouldn't satisfy these queries. If you query a hosted instance from your own app, match its ENSNode version with the same `enskit`/`enssdk` version. ```sh # from the ENSNode monorepo root pnpm install # set the VITE_ENSNODE_URL to a NameHash Hosted Instance and run this example in dev mode -VITE_ENSNODE_URL=https://api.alpha.ensnode.io pnpm -F enskit-react-example dev +VITE_ENSNODE_URL=https://api.v2-sepolia.blue.ensnode.io pnpm -F enskit-react-example dev ``` ## Usage (with Local ENSNode) diff --git a/examples/enskit-react-example/package.json b/examples/enskit-react-example/package.json index 1adc33966a..08c1bc1c97 100644 --- a/examples/enskit-react-example/package.json +++ b/examples/enskit-react-example/package.json @@ -12,8 +12,8 @@ "generate:gqlschema": "gql.tada generate-output" }, "dependencies": { - "enskit": "0.0.0-preview-fix-sha-89c022b-20260519094840", - "enssdk": "0.0.0-preview-fix-sha-89c022b-20260519094840", + "enskit": "workspace:*", + "enssdk": "workspace:*", "react": "catalog:", "react-dom": "catalog:", "react-router": "^7.6.1" diff --git a/examples/enskit-react-example/src/AccountView.tsx b/examples/enskit-react-example/src/AccountView.tsx index 1f90dba705..d0c67269a4 100644 --- a/examples/enskit-react-example/src/AccountView.tsx +++ b/examples/enskit-react-example/src/AccountView.tsx @@ -1,10 +1,5 @@ import { graphql, useOmnigraphQuery } from "enskit/react/omnigraph"; -import { - beautifyInterpretedName, - isNormalizedAddress, - type NormalizedAddress, - toNormalizedAddress, -} from "enssdk"; +import { isNormalizedAddress, type NormalizedAddress, toNormalizedAddress } from "enssdk"; import { useState } from "react"; import { Link, Navigate, useParams } from "react-router"; @@ -15,9 +10,7 @@ const AccountDomainsQuery = graphql(` domains(first: $first, after: $after) { totalCount edges { - # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - # node { __typename id canonical { name { interpreted } } } - node { __typename id name } + node { __typename id canonical { name { beautified } } } } pageInfo { hasNextPage endCursor } } @@ -62,15 +55,10 @@ function RenderAccount({ address }: { address: NormalizedAddress }) {
      {domains.edges.map((edge) => (
    • - {/* - TODO: after upgrading v2-sepolia to have materialized canonical name, update the label to: - - {beautifyInterpretedName(edge.node.canonical.name.interpreted)} - */} - {edge.node.name ? ( + {edge.node.canonical ? ( // link by DomainId so the exact ENSv1/ENSv2 variant the user clicked is preserved - {beautifyInterpretedName(edge.node.name)} + {edge.node.canonical.name.beautified} ) : ( non-canonical domain diff --git a/examples/enskit-react-example/src/App.tsx b/examples/enskit-react-example/src/App.tsx index a902c06260..51879ade73 100644 --- a/examples/enskit-react-example/src/App.tsx +++ b/examples/enskit-react-example/src/App.tsx @@ -13,7 +13,10 @@ const EXAMPLE_ACCOUNT_ADDRESS = "0x2f8e8b1126e75fde0b7f731e7cb5847eba2d2574"; // you may use a NameHash Hosted ENSNode instance // learn more at https://ensnode.io/docs/hosted-instances -const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL ?? "https://api.v2-sepolia.ensnode.io"; +// +// NOTE: we point at the `blue` deployment, which runs ENSNode 1.14.x — the version this example's +// queries target. The non-`blue` v2-sepolia instance still serves an older Omnigraph schema. +const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL ?? "https://api.v2-sepolia.blue.ensnode.io"; console.log(`Connecting to ENSNode at ${ENSNODE_URL}`); diff --git a/examples/enskit-react-example/src/DomainView.tsx b/examples/enskit-react-example/src/DomainView.tsx index 44ab27db65..9bfdc04b81 100644 --- a/examples/enskit-react-example/src/DomainView.tsx +++ b/examples/enskit-react-example/src/DomainView.tsx @@ -13,9 +13,8 @@ const DomainFragment = graphql(` fragment DomainFragment on Domain { __typename id - # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - # canonical { name { interpreted } } - name + # a Domain's Canonical Name; null when the Domain is not in the canonical nametree + canonical { name { beautified } } owner { id address } } `); @@ -28,9 +27,7 @@ const DomainByQuery = graphql( query DomainBy($by: DomainIdInput!, $first: Int!, $after: String) { domain(by: $by) { ...DomainFragment - # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - # parent { id canonical { name { interpreted } } } - parent { id name } + parent { id canonical { name { beautified } } } subdomains(first: $first, after: $after) { edges { node { @@ -84,9 +81,9 @@ function SubdomainLink({ data }: { data: FragmentOf }) { return (
    • - {domain.name ? ( + {domain.canonical ? ( // link by DomainId so the exact Domain (and its ENSv1/ENSv2 variant) is preserved - {beautifyInterpretedName(domain.name)} + {domain.canonical.name.beautified} ) : ( non-canonical domain )}{" "} @@ -124,11 +121,7 @@ function RenderDomain({ by }: { by: DomainBy }) { return (
      - {/* - TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: -

      {beautifyInterpretedName(domain.canonical?.name.interpreted ?? domain.id)}

      - */} -

      {domain.name ? beautifyInterpretedName(domain.name) : domain.id}

      +

      {domain.canonical ? domain.canonical.name.beautified : domain.id}

      Owner:{" "} {domain.owner ? ( @@ -141,17 +134,9 @@ function RenderDomain({ by }: { by: DomainBy }) {

      Version: {domain.__typename}

      - {/* - TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: {data.domain.parent?.canonical && ( - ← {beautifyInterpretedName(data.domain.parent.canonical.name.interpreted)} - - )} - */} - {data.domain.parent?.name && ( - - ← {beautifyInterpretedName(data.domain.parent.name)} + ← {data.domain.parent.canonical.name.beautified} )} diff --git a/examples/enskit-react-example/src/SearchView.tsx b/examples/enskit-react-example/src/SearchView.tsx index c4b65e9a14..6134e223c5 100644 --- a/examples/enskit-react-example/src/SearchView.tsx +++ b/examples/enskit-react-example/src/SearchView.tsx @@ -1,5 +1,4 @@ import { graphql, useOmnigraphQuery } from "enskit/react/omnigraph"; -import { beautifyInterpretedName } from "enssdk"; import { useEffect, useState } from "react"; import { Link, useSearchParams } from "react-router"; @@ -7,9 +6,7 @@ const DomainsByNameQuery = graphql(` query DomainsByName($name: String!, $first: Int!, $after: String) { domains(where: { name: $name }, first: $first, after: $after) { edges { - # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - # node { __typename id canonical { name { interpreted } } } - node { __typename id name } + node { __typename id canonical { name { beautified } } } } pageInfo { hasNextPage @@ -90,13 +87,13 @@ export function SearchView() { {fetching &&

      Loading...

      }
        {data?.domains?.edges.map((edge) => { - if (!edge.node.name) return null; + if (!edge.node.canonical) return null; return (
      • ({edge.node.__typename === "ENSv1Domain" ? "v1" : "v2"}){" "} {/* link by DomainId so the exact ENSv1/ENSv2 variant the user clicked is preserved */} - {beautifyInterpretedName(edge.node.name)} + {edge.node.canonical.name.beautified}
      • ); diff --git a/examples/enssdk-example/README.md b/examples/enssdk-example/README.md index 217f5c084f..2d33bcc82a 100644 --- a/examples/enssdk-example/README.md +++ b/examples/enssdk-example/README.md @@ -6,13 +6,13 @@ Companion to the [enssdk integration guide](https://ensnode.io/docs/integrate/in ## Usage (with NameHash Hosted Instance) -> **Version compatibility:** Our hosted ENSNode instances currently run ENSNode v1.13. If you are querying them from your own app, you **must** use `enssdk@1.13.1`. The latest published version (`1.14.0+`) contains breaking changes in the Omnigraph API data model not yet deployed to our hosted infrastructure. This notice will be removed once the hosted instances are upgraded. +> **Schema version:** This example tracks the latest Omnigraph schema (ENSNode 1.14.x) via `workspace:*` `enssdk`. It queries the `blue` hosted deployment, which runs 1.14.x; the default (non-`blue`) hosted instances still serve an older schema. If you query a hosted instance from your own app, match its ENSNode version with the same `enssdk` version. ```sh # from the ENSNode monorepo root pnpm install -ENSNODE_URL=https://api.alpha.ensnode.io pnpm -F enssdk-example start +ENSNODE_URL=https://api.v2-sepolia.blue.ensnode.io pnpm -F enssdk-example start ```` ## Usage (with Local ENSNode) diff --git a/examples/enssdk-example/package.json b/examples/enssdk-example/package.json index 436cf56f40..72c39de7a7 100644 --- a/examples/enssdk-example/package.json +++ b/examples/enssdk-example/package.json @@ -10,7 +10,7 @@ "generate:gqlschema": "gql.tada generate-output" }, "dependencies": { - "enssdk": "0.0.0-preview-fix-sha-89c022b-20260519094840" + "enssdk": "workspace:*" }, "devDependencies": { "@types/node": "catalog:", diff --git a/examples/enssdk-example/src/index.ts b/examples/enssdk-example/src/index.ts index 57f078f4e7..f52c23163f 100644 --- a/examples/enssdk-example/src/index.ts +++ b/examples/enssdk-example/src/index.ts @@ -1,4 +1,4 @@ -import { asInterpretedName, beautifyInterpretedName } from "enssdk"; +import { asInterpretedName } from "enssdk"; import { createEnsNodeClient } from "enssdk/core"; import { type FragmentOf, graphql, omnigraph, readFragment } from "enssdk/omnigraph"; @@ -13,9 +13,8 @@ const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph); const DomainFragment = graphql(` fragment DomainFragment on Domain { __typename - # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - # canonical { name { interpreted } } - name + # a Domain's Canonical Name; null when the Domain is not in the canonical nametree + canonical { name { beautified } } owner { address } } `); @@ -38,11 +37,7 @@ const HelloWorldQuery = graphql( function formatDomain(data: FragmentOf): string { // type-safe access to fragment data! const domain = readFragment(DomainFragment, data); - // TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - // const name = domain.canonical - // ? beautifyInterpretedName(domain.canonical.name.interpreted) - // : ""; - const name = domain.name ? beautifyInterpretedName(domain.name) : ""; + const name = domain.canonical ? domain.canonical.name.beautified : ""; const owner = domain.owner?.address ?? "0x0 (means reserved for ENSv2)"; return `${name} (${domain.__typename}) — Owner ${owner}`; } diff --git a/examples/omnigraph-graphql-example/README.md b/examples/omnigraph-graphql-example/README.md index 80b4365d70..e619ab1808 100644 --- a/examples/omnigraph-graphql-example/README.md +++ b/examples/omnigraph-graphql-example/README.md @@ -8,13 +8,13 @@ Companion to the [ENS Omnigraph GraphQL API integration guide](https://ensnode.i ## Usage (with NameHash Hosted Instance) -> **Version compatibility:** Our hosted ENSNode instances currently run ENSNode v1.13. If you are querying them from your own app, you **must** use `enssdk@1.13.1` (and `enskit@1.13.1` when using React). The latest published versions (`1.14.0+`) contain breaking changes in the Omnigraph API data model not yet deployed to our hosted infrastructure. This notice will be removed once the hosted instances are upgraded. +> **Schema version:** This example targets the latest Omnigraph schema (ENSNode 1.14.x). It queries the `blue` hosted deployment, which runs 1.14.x; the default (non-`blue`) hosted instances still serve an older schema. The Omnigraph schema is versioned with ENSNode, so point this example at a deployment whose version matches the queries below. ```sh # from the ENSNode monorepo root pnpm install -ENSNODE_URL=https://api.alpha.ensnode.io pnpm -F omnigraph-graphql-example start +ENSNODE_URL=https://api.v2-sepolia.blue.ensnode.io pnpm -F omnigraph-graphql-example start ``` ## Usage (with Local ENSNode) diff --git a/examples/omnigraph-graphql-example/src/index.ts b/examples/omnigraph-graphql-example/src/index.ts index 071ac272aa..3ce03b71d3 100644 --- a/examples/omnigraph-graphql-example/src/index.ts +++ b/examples/omnigraph-graphql-example/src/index.ts @@ -9,15 +9,12 @@ const HELLO_WORLD_QUERY = /* GraphQL */ ` query HelloWorld($name: InterpretedName!) { domain(by: { name: $name }) { __typename - # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - # canonical { name { interpreted } } - name + # a Domain's Canonical Name; null when the Domain is not in the canonical nametree + canonical { name { beautified } } owner { address } subdomains(first: 20) { totalCount - # # TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - # edges { node { __typename canonical { name { interpreted } } owner { address } } } - edges { node { __typename name owner { address } } } + edges { node { __typename canonical { name { beautified } } owner { address } } } } } } @@ -25,9 +22,7 @@ const HELLO_WORLD_QUERY = /* GraphQL */ ` interface Domain { __typename: "ENSv1Domain" | "ENSv2Domain"; - // TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - // canonical: { name: { interpreted: string } } | null; - name: string; + canonical: { name: { beautified: string } } | null; owner: { address: string } | null; } @@ -46,9 +41,7 @@ interface QueryResult { } function formatDomain(domain: Domain): string { - // TODO: after upgrading v2-sepolia to have materialized canonical name, update this to: - // const name = domain.canonical?.name.interpreted ?? ""; - const name = domain.name ?? ""; + const name = domain.canonical?.name.beautified ?? ""; const owner = domain.owner?.address ?? "0x0"; return `${name} (${domain.__typename}) — Owner ${owner}`; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a507dc016..0b23dbaf62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -844,11 +844,11 @@ importers: examples/enskit-react-example: dependencies: enskit: - specifier: 0.0.0-preview-fix-sha-89c022b-20260519094840 - version: 0.0.0-preview-fix-sha-89c022b-20260519094840(gql.tada@1.9.1(graphql@16.11.0)(typescript@5.9.3))(graphql@16.11.0)(react@19.2.1)(viem@2.50.3(typescript@5.9.3)(zod@4.3.6)) + specifier: workspace:* + version: link:../../packages/enskit enssdk: - specifier: 0.0.0-preview-fix-sha-89c022b-20260519094840 - version: 0.0.0-preview-fix-sha-89c022b-20260519094840(gql.tada@1.9.1(graphql@16.11.0)(typescript@5.9.3))(graphql@16.11.0)(viem@2.50.3(typescript@5.9.3)(zod@4.3.6)) + specifier: workspace:* + version: link:../../packages/enssdk react: specifier: 'catalog:' version: 19.2.1 @@ -881,8 +881,8 @@ importers: examples/enssdk-example: dependencies: enssdk: - specifier: 0.0.0-preview-fix-sha-89c022b-20260519094840 - version: 0.0.0-preview-fix-sha-89c022b-20260519094840(gql.tada@1.9.1(graphql@16.11.0)(typescript@5.9.3))(graphql@16.11.0)(viem@2.50.3(typescript@5.9.3)(zod@4.3.6)) + specifier: workspace:* + version: link:../../packages/enssdk devDependencies: '@types/node': specifier: 'catalog:' @@ -6518,21 +6518,6 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} - enskit@0.0.0-preview-fix-sha-89c022b-20260519094840: - resolution: {integrity: sha512-tfHXorVpsUbl/ft4HXw7ls/X+vdwoYQepK+VgYRN7QKvHFarLnnJvh2l4uO74nfsEYfDLX5syY5TDmbpwyZcvg==} - peerDependencies: - gql.tada: ^1.8.10 - graphql: ^16 - react: ^18.0.0 || ^19.0.0 - viem: ^2 - - enssdk@0.0.0-preview-fix-sha-89c022b-20260519094840: - resolution: {integrity: sha512-bYpPTEWUSS3ESkb7hfGjHj7DkjFiJJLayo2gf3FfT+IzRb/gcOZ5eMCUtXF+TxWxaPgCMLBRbSfdsJqKsosWCw==} - peerDependencies: - gql.tada: ^1.8.10 - graphql: ^16 - viem: ^2 - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -16227,26 +16212,6 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - enskit@0.0.0-preview-fix-sha-89c022b-20260519094840(gql.tada@1.9.1(graphql@16.11.0)(typescript@5.9.3))(graphql@16.11.0)(react@19.2.1)(viem@2.50.3(typescript@5.9.3)(zod@4.3.6)): - dependencies: - '@urql/core': 6.0.1(graphql@16.11.0) - '@urql/exchange-graphcache': 9.0.0(@urql/core@6.0.1(graphql@16.11.0))(graphql@16.11.0) - enssdk: 0.0.0-preview-fix-sha-89c022b-20260519094840(gql.tada@1.9.1(graphql@16.11.0)(typescript@5.9.3))(graphql@16.11.0)(viem@2.50.3(typescript@5.9.3)(zod@4.3.6)) - gql.tada: 1.9.1(graphql@16.11.0)(typescript@5.9.3) - graphql: 16.11.0 - react: 19.2.1 - urql: 5.0.1(@urql/core@6.0.1(graphql@16.11.0))(react@19.2.1) - viem: 2.50.3(typescript@5.9.3)(zod@4.3.6) - - enssdk@0.0.0-preview-fix-sha-89c022b-20260519094840(gql.tada@1.9.1(graphql@16.11.0)(typescript@5.9.3))(graphql@16.11.0)(viem@2.50.3(typescript@5.9.3)(zod@4.3.6)): - dependencies: - '@adraffy/ens-normalize': 1.11.1 - '@ensdomains/address-encoder': 1.1.4 - caip: 1.1.1 - gql.tada: 1.9.1(graphql@16.11.0)(typescript@5.9.3) - graphql: 16.11.0 - viem: 2.50.3(typescript@5.9.3)(zod@4.3.6) - entities@4.5.0: {} entities@6.0.1: {} From ddf3bd5f1355767fef04583238c028e41b3c1459 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 22 May 2026 11:04:52 -0500 Subject: [PATCH 04/13] docs: version-lock Omnigraph walkthroughs and schema reference to active version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The schema reference rendered the live `main` SDL while examples were locked to the production-deployed version; the walkthroughs hardcoded 1.14.x schema/code against production endpoints running 1.13.1 — so both were broken against production. - Schema reference (OmnigraphSchemaDocExplorer) now loads the active version's frozen schema.graphql instead of `enssdk/omnigraph/schema.graphql?raw`. - Walkthroughs (omnigraph-graphql-api, enssdk, enskit) are split into per-version MDX partials under @components/walkthroughs//, with the route page rendering the partial matching ACTIVE_OMNIGRAPH_VERSION. Authored v1.13.1 (production) and v1.14.1 (blue) variants; promote by bumping the constant. - HostedInstanceSdkVersionWarning derives the pinned SDK version from the active version. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../HostedInstanceSdkVersionWarning.astro | 25 +- .../organisms/OmnigraphSchemaDocExplorer.tsx | 18 +- .../walkthroughs/enskit/v1.13.1.mdx | 412 +++++++++++++++++ .../walkthroughs/enskit/v1.14.1.mdx | 412 +++++++++++++++++ .../walkthroughs/enssdk/v1.13.1.mdx | 312 +++++++++++++ .../walkthroughs/enssdk/v1.14.1.mdx | 312 +++++++++++++ .../omnigraph-graphql-api/v1.13.1.mdx | 182 ++++++++ .../omnigraph-graphql-api/v1.14.1.mdx | 182 ++++++++ .../integration-options/enskit/index.mdx | 421 +----------------- .../integration-options/enssdk/index.mdx | 321 +------------ .../omnigraph-graphql-api.mdx | 194 +------- 11 files changed, 1874 insertions(+), 917 deletions(-) create mode 100644 docs/ensnode.io/src/components/walkthroughs/enskit/v1.13.1.mdx create mode 100644 docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx create mode 100644 docs/ensnode.io/src/components/walkthroughs/enssdk/v1.13.1.mdx create mode 100644 docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx create mode 100644 docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.13.1.mdx create mode 100644 docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx diff --git a/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro b/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro index f05ea3fb3e..c40e9eaeda 100644 --- a/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro +++ b/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro @@ -1,6 +1,8 @@ --- import { Aside } from "@astrojs/starlight/components"; +import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active"; + interface Props { /** * Which SDK(s) the warning is for. @@ -16,25 +18,28 @@ const { for: target = "enssdk" } = Astro.props; const isEnskit = target === "enskit"; const isBoth = target === "both"; +// The SDK version is locked to the production-deployed Omnigraph version (see +// `@data/omnigraph-examples/active`). The SDK bundles the Omnigraph schema, so pinning the matching +// version keeps gql.tada's generated types aligned with the deployed API. Updates on promotion. +const sdkVersion = ACTIVE_OMNIGRAPH_VERSION.replace(/^v/, ""); + const sdkList = isEnskit - ? "enskit@1.13.1 and enssdk@1.13.1" + ? `enskit@${sdkVersion} and enssdk@${sdkVersion}` : isBoth - ? "enssdk@1.13.1 (and enskit@1.13.1 when using React)" - : "enssdk@1.13.1"; + ? `enssdk@${sdkVersion} (and enskit@${sdkVersion} when using React)` + : `enssdk@${sdkVersion}`; --- diff --git a/docs/ensnode.io/src/components/organisms/OmnigraphSchemaDocExplorer.tsx b/docs/ensnode.io/src/components/organisms/OmnigraphSchemaDocExplorer.tsx index 4fd5e68419..000bf6d425 100644 --- a/docs/ensnode.io/src/components/organisms/OmnigraphSchemaDocExplorer.tsx +++ b/docs/ensnode.io/src/components/organisms/OmnigraphSchemaDocExplorer.tsx @@ -3,8 +3,24 @@ import "@graphiql/plugin-doc-explorer/style.css"; import { DocExplorer, DocExplorerStore } from "@graphiql/plugin-doc-explorer"; import { GraphiQLProvider } from "@graphiql/react"; -import omnigraphSchemaSdl from "enssdk/omnigraph/schema.graphql?raw"; import { buildSchema } from "graphql"; +import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active"; + +// Render the schema for the production-locked Omnigraph version (the same version the examples and +// walkthroughs target), NOT the live `main` SDL — `main` runs ahead of production. Schemas are frozen +// per-version under `src/data/omnigraph-examples/versions//schema.graphql`; glob all (Vite +// can't import a runtime-variable path) and select the active one. +const schemasByVersion = import.meta.glob( + "../../data/omnigraph-examples/versions/*/schema.graphql", + { query: "?raw", import: "default", eager: true }, +); +const omnigraphSchemaSdl = + schemasByVersion[ + `../../data/omnigraph-examples/versions/${ACTIVE_OMNIGRAPH_VERSION}/schema.graphql` + ]; +if (!omnigraphSchemaSdl) { + throw new Error(`No Omnigraph schema snapshot for version "${ACTIVE_OMNIGRAPH_VERSION}".`); +} const omnigraphSchema = buildSchema(omnigraphSchemaSdl); diff --git a/docs/ensnode.io/src/components/walkthroughs/enskit/v1.13.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.13.1.mdx new file mode 100644 index 0000000000..80b1d87a90 --- /dev/null +++ b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.13.1.mdx @@ -0,0 +1,412 @@ +import { LinkCard, Steps } from '@astrojs/starlight/components'; +import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro'; + +`enskit` is the React toolkit for ENSv2 development. It provides a fully typed Omnigraph API client (powered by [`urql`](https://nearform.com/open-source/urql/) and [`gql.tada`](https://gql-tada.0no.co/)), the `OmnigraphProvider`, and the `useOmnigraphQuery` hook for writing type-safe ENS queries with editor autocomplete, Relay-style pagination, and Omnigraph-specific cache directives. + +This guide walks you from an empty directory to a working React component that renders an [ENS Domain](/docs/concepts/the-ens-protocol) and a paginated list of its subdomains — the same flow as the [`DomainView`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in our example app. + + + + +## 1. Scaffold a React app + +If you already have a React + TypeScript app, skip ahead to [Install `enskit` and `enssdk`](#2-install-enskit-and-enssdk). + +Otherwise, the fastest way to get going is [Vite](https://vite.dev): + +```sh +npm create vite@latest my-ens-app -- --template react-ts +cd my-ens-app +npm install +``` + +## 2. Install `enskit` and `enssdk` + +```sh +npm install enskit@1.13.1 enssdk@1.13.1 +``` + +:::caution[Pin exact versions] +Always pin **exact** versions (no `^` or `~`) of `enskit` and `enssdk`, and keep them on the same version. The Omnigraph GraphQL schema is bundled inside `enssdk` and consumed by the `gql.tada` TypeScript plugin to type your queries — a minor or patch bump can change the schema and silently drift your generated types away from your queries. Locking exact versions keeps types and runtime in sync, and matched to the ENSNode version your hosted instance runs. +::: + +## 3. Configure the `gql.tada` TypeScript plugin + +`gql.tada` is what gives your `graphql(...)` query strings end-to-end type safety. It reads the Omnigraph schema from `enssdk` at typecheck time. + +Add the plugin to `tsconfig.json`: + +```json title="tsconfig.json" {6-13} +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "plugins": [ + { + "name": "gql.tada/ts-plugin", + "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql", + "tadaOutputLocation": "./src/generated/graphql-env.d.ts" + } + ] + }, + "include": ["src"] +} +``` + +If you're using VS Code, make sure your workspace is using the workspace TypeScript version so the plugin loads. Add this to `.vscode/settings.json`: + +```json title=".vscode/settings.json" +{ + "js/ts.tsdk.path": "node_modules/typescript/lib", + "js/ts.tsdk.promptToUseWorkspaceVersion": true +} +``` + +## 4. Mount the `OmnigraphProvider` + +`OmnigraphProvider` is what `useOmnigraphQuery` reads from. Construct an `EnsNodeClient`, extend it with the `omnigraph` module, and wrap your app: + +```tsx title="src/App.tsx" +import { OmnigraphProvider } from "enskit/react/omnigraph"; +import { createEnsNodeClient } from "enssdk/core"; +import { omnigraph } from "enssdk/omnigraph"; +import { StrictMode } from "react"; + +import { DomainView } from "./DomainView"; + +// you may use a NameHash Hosted ENSNode instance +// learn more at https://ensnode.io/docs/hosted-instances +const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL! + +// create and extend an EnsNodeClient with Omnigraph support +const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph); + +export function App() { + return ( + + +

        My ENS App

        + +
        +
        + ); +} +``` + +## 5. Hello world + +Create `src/DomainView.tsx`. We'll start with the simplest possible query — look up the `eth` Domain and render its owner and protocol version. + +:::tip[InterpretedName] +An **InterpretedName** is a Name whose labels are each either normalized or represented as an [encoded labelhash](/docs/reference/terminology#encoded-labelhash) (e.g. `[abcd...].eth`) — the canonical, lossless form ENSNode uses to identify a Name. `asInterpretedName("eth")` brands a known-safe string as one; for user input, validate first. See [Interpreted Name](/docs/reference/terminology#interpreted-name) in the terminology reference. +::: + +```tsx title="src/DomainView.tsx" +import { graphql, useOmnigraphQuery } from "enskit/react/omnigraph"; +import { asInterpretedName, beautifyInterpretedName } from "enssdk"; + +const DomainByNameQuery = graphql(` + query DomainByName($name: InterpretedName!) { + domain(by: { name: $name }) { + __typename + name + owner { address } + } + } +`); + +export function DomainView() { + const name = asInterpretedName("eth"); + + const [result] = useOmnigraphQuery({ + query: DomainByNameQuery, + variables: { name }, + }); + + const { data, fetching, error } = result; + + if (!data && fetching) return

        Loading...

        ; + if (error) return

        Error: {error.message}

        ; + if (!data?.domain) return

        No domain found.

        ; + + const { domain } = data; + + return ( +
        +

        + {domain.name + ? beautifyInterpretedName(domain.name) + : "Unnamed Domain"} +

        +

        Version: {domain.__typename}

        +

        + Owner: {domain.owner?.address ?? "0x0"} +

        +
        + ); +} +``` + +A few things to notice: + +- `graphql(...)` parses your query at typecheck time. Hover over `result.data` and you'll see it's typed exactly to your selection set — try removing `owner { address }` from the query and watch the access below become a type error. +- `domain` is a union of `ENSv1Domain | ENSv2Domain` (both implement the `Domain` interface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query — `__typename` tells you which one you got. +- `name` may be `null` for non-canonical names (e.g. Domains whose name cannot be inferred). **Always** guard the access; TypeScript will help you. + +## 6. List subdomains + +Expand the query to also fetch the Domain's subdomains. `subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm), so the shape is `{ edges: [{ node }] }`. + +```tsx title="src/DomainView.tsx" ins={7-13,40-54} +const DomainByNameQuery = graphql(` + query DomainByName($name: InterpretedName!) { + domain(by: { name: $name }) { + __typename + name + owner { address } + subdomains { + edges { + node { + name + owner { address } + } + } + } + } + } +`); + +export function DomainView() { + const name = asInterpretedName("eth"); + + const [result] = useOmnigraphQuery({ + query: DomainByNameQuery, + variables: { name }, + }); + + const { data, fetching, error } = result; + + if (!data && fetching) return

        Loading...

        ; + if (error) return

        Error: {error.message}

        ; + if (!data?.domain) return

        No domain found.

        ; + + const { domain } = data; + + return ( +
        +

        {domain.name ? beautifyInterpretedName(domain.name) : "Unnamed Domain"}

        +

        Version: {domain.__typename}

        +

        Owner: {domain.owner?.address ?? "0x0"}

        + +

        Subdomains

        +
          + {domain.subdomains?.edges.map(({ node }, i) => ( +
        • + {node.name + ? beautifyInterpretedName(node.name) + : unnamed}{" "} + — Owner {node.owner?.address ?? "0x0"} +
        • + ))} +
        +
        + ); +} +``` + +## 7. Extract a typed fragment + +Notice we're selecting the same fields (`name`, `owner { address }`) on the parent Domain _and_ on each subdomain. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed shape for components that render a Domain. + +```tsx title="src/DomainView.tsx" ins={1,2,5,9-15,21,23,28,31-48,66,72} +import { + type FragmentOf, + graphql, + readFragment, + useOmnigraphQuery, +} from "enskit/react/omnigraph"; +import { asInterpretedName, beautifyInterpretedName } from "enssdk"; + +const DomainFragment = graphql(` + fragment DomainFragment on Domain { + __typename + name + owner { address } + } +`); + +const DomainByNameQuery = graphql( + ` + query DomainByName($name: InterpretedName!) { + domain(by: { name: $name }) { + ...DomainFragment + subdomains { + edges { node { ...DomainFragment } } + } + } + } +`, + [DomainFragment], +); + +function RenderDomain({ data }: { data: FragmentOf }) { + // type-safe access to fragment data! + const domain = readFragment(DomainFragment, data); + + return ( + <> + + {domain.name + ? beautifyInterpretedName(domain.name) + : "Unnamed Domain"} + {" "} + ({domain.__typename}){" "} + + — Owner {domain.owner?.address ?? "0x0"} + + + ); +} + +export function DomainView() { + const name = asInterpretedName("eth"); + + const [result] = useOmnigraphQuery({ + query: DomainByNameQuery, + variables: { name }, + }); + + const { data, fetching, error } = result; + + if (!data && fetching) return

        Loading...

        ; + if (error) return

        Error: {error.message}

        ; + if (!data?.domain) return

        No domain found.

        ; + + return ( +
        +

        + +

        Subdomains

        +
          + {data.domain.subdomains?.edges.map(({ node }, i) => ( +
        • + +
        • + ))} +
        +
        + ); +} +``` + +`FragmentOf` is the opaque type for any selection that includes `...DomainFragment` — `RenderDomain` accepts any of them. `readFragment(DomainFragment, data)` unwraps that opaque type to the typed fields you declared. + +## 8. Paginate with "Load more" + +`subdomains` is a Relay Connection — page through it with the `first` and `after` arguments. Add `pageInfo { hasNextPage endCursor }` to the query, track the cursor in component state, and wire up a "Next page" button. + +```tsx title="src/DomainView.tsx" ins={1,6,9,11,19,23,27,51-59} +import { useState } from "react"; +// ...other imports + +const DomainByNameQuery = graphql( + ` + query DomainByName($name: InterpretedName!, $first: Int!, $after: String) { + domain(by: { name: $name }) { + ...DomainFragment + subdomains(first: $first, after: $after) { + edges { node { ...DomainFragment } } + pageInfo { hasNextPage endCursor } + } + } + } +`, + [DomainFragment], +); + +const PAGE_SIZE = 20; + +export function DomainView() { + const name = asInterpretedName("eth"); + const [after, setAfter] = useState(null); + + const [result] = useOmnigraphQuery({ + query: DomainByNameQuery, + variables: { name, first: PAGE_SIZE, after }, + }); + + const { data, fetching, error } = result; + + if (!data && fetching) return

        Loading...

        ; + if (error) return

        Error: {error.message}

        ; + if (!data?.domain) return

        No domain found.

        ; + + const { subdomains } = data.domain; + + return ( +
        +

        + +

        Subdomains

        +
          + {subdomains?.edges.map(({ node }, i) => ( +
        • + +
        • + ))} +
        + + {subdomains?.pageInfo.hasNextPage && ( + + )} +
        + ); +} +``` + +## 9. Run it + +```sh +VITE_ENSNODE_URL=https://api.v2-sepolia.ensnode.io npm run dev +``` + +Open the printed URL and you should see the `eth` Domain, its owner, and the first page of its subdomains. Clicking **Next page** advances the cursor. + +## Where to go next + +- Try the [Interactive Example](/docs/integrate/integration-options/enskit/example): edit and run the full `enskit-react-example` app in your browser with a live preview. +- Swap the hardcoded `"eth"` for a name from props or a router — see [`EnsureInterpretedName`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in the example app for safe handling of user-provided names. +- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more. +- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query. +- Need data outside React? Use [`enssdk`](/docs/integrate/integration-options/enssdk) directly with the same `graphql(...)` helper. + + + + + + + + diff --git a/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx new file mode 100644 index 0000000000..09af7d51c6 --- /dev/null +++ b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx @@ -0,0 +1,412 @@ +import { LinkCard, Steps } from '@astrojs/starlight/components'; +import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro'; + +`enskit` is the React toolkit for ENSv2 development. It provides a fully typed Omnigraph API client (powered by [`urql`](https://nearform.com/open-source/urql/) and [`gql.tada`](https://gql-tada.0no.co/)), the `OmnigraphProvider`, and the `useOmnigraphQuery` hook for writing type-safe ENS queries with editor autocomplete, Relay-style pagination, and Omnigraph-specific cache directives. + +This guide walks you from an empty directory to a working React component that renders an [ENS Domain](/docs/concepts/the-ens-protocol) and a paginated list of its subdomains — the same flow as the [`DomainView`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in our example app. + + + + +## 1. Scaffold a React app + +If you already have a React + TypeScript app, skip ahead to [Install `enskit` and `enssdk`](#2-install-enskit-and-enssdk). + +Otherwise, the fastest way to get going is [Vite](https://vite.dev): + +```sh +npm create vite@latest my-ens-app -- --template react-ts +cd my-ens-app +npm install +``` + +## 2. Install `enskit` and `enssdk` + +```sh +npm install enskit@1.14.1 enssdk@1.14.1 +``` + +:::caution[Pin exact versions] +Always pin **exact** versions (no `^` or `~`) of `enskit` and `enssdk`, and keep them on the same version. The Omnigraph GraphQL schema is bundled inside `enssdk` and consumed by the `gql.tada` TypeScript plugin to type your queries — a minor or patch bump can change the schema and silently drift your generated types away from your queries. Locking exact versions keeps types and runtime in sync, and matched to the ENSNode version your hosted instance runs. +::: + +## 3. Configure the `gql.tada` TypeScript plugin + +`gql.tada` is what gives your `graphql(...)` query strings end-to-end type safety. It reads the Omnigraph schema from `enssdk` at typecheck time. + +Add the plugin to `tsconfig.json`: + +```json title="tsconfig.json" {6-13} +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "plugins": [ + { + "name": "gql.tada/ts-plugin", + "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql", + "tadaOutputLocation": "./src/generated/graphql-env.d.ts" + } + ] + }, + "include": ["src"] +} +``` + +If you're using VS Code, make sure your workspace is using the workspace TypeScript version so the plugin loads. Add this to `.vscode/settings.json`: + +```json title=".vscode/settings.json" +{ + "js/ts.tsdk.path": "node_modules/typescript/lib", + "js/ts.tsdk.promptToUseWorkspaceVersion": true +} +``` + +## 4. Mount the `OmnigraphProvider` + +`OmnigraphProvider` is what `useOmnigraphQuery` reads from. Construct an `EnsNodeClient`, extend it with the `omnigraph` module, and wrap your app: + +```tsx title="src/App.tsx" +import { OmnigraphProvider } from "enskit/react/omnigraph"; +import { createEnsNodeClient } from "enssdk/core"; +import { omnigraph } from "enssdk/omnigraph"; +import { StrictMode } from "react"; + +import { DomainView } from "./DomainView"; + +// you may use a NameHash Hosted ENSNode instance +// learn more at https://ensnode.io/docs/hosted-instances +const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL! + +// create and extend an EnsNodeClient with Omnigraph support +const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph); + +export function App() { + return ( + + +

        My ENS App

        + +
        +
        + ); +} +``` + +## 5. Hello world + +Create `src/DomainView.tsx`. We'll start with the simplest possible query — look up the `eth` Domain and render its owner and protocol version. + +:::tip[InterpretedName] +An **InterpretedName** is a Name whose labels are each either normalized or represented as an [encoded labelhash](/docs/reference/terminology#encoded-labelhash) (e.g. `[abcd...].eth`) — the canonical, lossless form ENSNode uses to identify a Name. `asInterpretedName("eth")` brands a known-safe string as one; for user input, validate first. See [Interpreted Name](/docs/reference/terminology#interpreted-name) in the terminology reference. +::: + +```tsx title="src/DomainView.tsx" +import { graphql, useOmnigraphQuery } from "enskit/react/omnigraph"; +import { asInterpretedName, beautifyInterpretedName } from "enssdk"; + +const DomainByNameQuery = graphql(` + query DomainByName($name: InterpretedName!) { + domain(by: { name: $name }) { + __typename + canonical { name { interpreted } } + owner { address } + } + } +`); + +export function DomainView() { + const name = asInterpretedName("eth"); + + const [result] = useOmnigraphQuery({ + query: DomainByNameQuery, + variables: { name }, + }); + + const { data, fetching, error } = result; + + if (!data && fetching) return

        Loading...

        ; + if (error) return

        Error: {error.message}

        ; + if (!data?.domain) return

        No domain found.

        ; + + const { domain } = data; + + return ( +
        +

        + {domain.canonical + ? beautifyInterpretedName(domain.canonical.name.interpreted) + : "Unnamed Domain"} +

        +

        Version: {domain.__typename}

        +

        + Owner: {domain.owner?.address ?? "0x0"} +

        +
        + ); +} +``` + +A few things to notice: + +- `graphql(...)` parses your query at typecheck time. Hover over `result.data` and you'll see it's typed exactly to your selection set — try removing `owner { address }` from the query and watch the access below become a type error. +- `domain` is a union of `ENSv1Domain | ENSv2Domain` (both implement the `Domain` interface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query — `__typename` tells you which one you got. +- `canonical` may be `null` for non-canonical names (e.g. Domains whose name cannot be inferred). **Always** guard the access; TypeScript will help you. + +## 6. List subdomains + +Expand the query to also fetch the Domain's subdomains. `subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm), so the shape is `{ edges: [{ node }] }`. + +```tsx title="src/DomainView.tsx" ins={7-13,40-54} +const DomainByNameQuery = graphql(` + query DomainByName($name: InterpretedName!) { + domain(by: { name: $name }) { + __typename + canonical { name { interpreted } } + owner { address } + subdomains { + edges { + node { + canonical { name { interpreted } } + owner { address } + } + } + } + } + } +`); + +export function DomainView() { + const name = asInterpretedName("eth"); + + const [result] = useOmnigraphQuery({ + query: DomainByNameQuery, + variables: { name }, + }); + + const { data, fetching, error } = result; + + if (!data && fetching) return

        Loading...

        ; + if (error) return

        Error: {error.message}

        ; + if (!data?.domain) return

        No domain found.

        ; + + const { domain } = data; + + return ( +
        +

        {domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : "Unnamed Domain"}

        +

        Version: {domain.__typename}

        +

        Owner: {domain.owner?.address ?? "0x0"}

        + +

        Subdomains

        +
          + {domain.subdomains?.edges.map(({ node }, i) => ( +
        • + {node.canonical + ? beautifyInterpretedName(node.canonical.name.interpreted) + : unnamed}{" "} + — Owner {node.owner?.address ?? "0x0"} +
        • + ))} +
        +
        + ); +} +``` + +## 7. Extract a typed fragment + +Notice we're selecting the same fields (`canonical { name { interpreted } }`, `owner { address }`) on the parent Domain _and_ on each subdomain. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed shape for components that render a Domain. + +```tsx title="src/DomainView.tsx" ins={1,2,5,9-15,21,23,28,31-48,66,72} +import { + type FragmentOf, + graphql, + readFragment, + useOmnigraphQuery, +} from "enskit/react/omnigraph"; +import { asInterpretedName, beautifyInterpretedName } from "enssdk"; + +const DomainFragment = graphql(` + fragment DomainFragment on Domain { + __typename + canonical { name { interpreted } } + owner { address } + } +`); + +const DomainByNameQuery = graphql( + ` + query DomainByName($name: InterpretedName!) { + domain(by: { name: $name }) { + ...DomainFragment + subdomains { + edges { node { ...DomainFragment } } + } + } + } +`, + [DomainFragment], +); + +function RenderDomain({ data }: { data: FragmentOf }) { + // type-safe access to fragment data! + const domain = readFragment(DomainFragment, data); + + return ( + <> + + {domain.canonical + ? beautifyInterpretedName(domain.canonical.name.interpreted) + : "Unnamed Domain"} + {" "} + ({domain.__typename}){" "} + + — Owner {domain.owner?.address ?? "0x0"} + + + ); +} + +export function DomainView() { + const name = asInterpretedName("eth"); + + const [result] = useOmnigraphQuery({ + query: DomainByNameQuery, + variables: { name }, + }); + + const { data, fetching, error } = result; + + if (!data && fetching) return

        Loading...

        ; + if (error) return

        Error: {error.message}

        ; + if (!data?.domain) return

        No domain found.

        ; + + return ( +
        +

        + +

        Subdomains

        +
          + {data.domain.subdomains?.edges.map(({ node }, i) => ( +
        • + +
        • + ))} +
        +
        + ); +} +``` + +`FragmentOf` is the opaque type for any selection that includes `...DomainFragment` — `RenderDomain` accepts any of them. `readFragment(DomainFragment, data)` unwraps that opaque type to the typed fields you declared. + +## 8. Paginate with "Load more" + +`subdomains` is a Relay Connection — page through it with the `first` and `after` arguments. Add `pageInfo { hasNextPage endCursor }` to the query, track the cursor in component state, and wire up a "Next page" button. + +```tsx title="src/DomainView.tsx" ins={1,6,9,11,19,23,27,51-59} +import { useState } from "react"; +// ...other imports + +const DomainByNameQuery = graphql( + ` + query DomainByName($name: InterpretedName!, $first: Int!, $after: String) { + domain(by: { name: $name }) { + ...DomainFragment + subdomains(first: $first, after: $after) { + edges { node { ...DomainFragment } } + pageInfo { hasNextPage endCursor } + } + } + } +`, + [DomainFragment], +); + +const PAGE_SIZE = 20; + +export function DomainView() { + const name = asInterpretedName("eth"); + const [after, setAfter] = useState(null); + + const [result] = useOmnigraphQuery({ + query: DomainByNameQuery, + variables: { name, first: PAGE_SIZE, after }, + }); + + const { data, fetching, error } = result; + + if (!data && fetching) return

        Loading...

        ; + if (error) return

        Error: {error.message}

        ; + if (!data?.domain) return

        No domain found.

        ; + + const { subdomains } = data.domain; + + return ( +
        +

        + +

        Subdomains

        +
          + {subdomains?.edges.map(({ node }, i) => ( +
        • + +
        • + ))} +
        + + {subdomains?.pageInfo.hasNextPage && ( + + )} +
        + ); +} +``` + +## 9. Run it + +```sh +VITE_ENSNODE_URL=https://api.v2-sepolia.blue.ensnode.io npm run dev +``` + +Open the printed URL and you should see the `eth` Domain, its owner, and the first page of its subdomains. Clicking **Next page** advances the cursor. + +## Where to go next + +- Try the [Interactive Example](/docs/integrate/integration-options/enskit/example): edit and run the full `enskit-react-example` app in your browser with a live preview. +- Swap the hardcoded `"eth"` for a name from props or a router — see [`EnsureInterpretedName`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in the example app for safe handling of user-provided names. +- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more. +- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query. +- Need data outside React? Use [`enssdk`](/docs/integrate/integration-options/enssdk) directly with the same `graphql(...)` helper. + + + + + + + + diff --git a/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.13.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.13.1.mdx new file mode 100644 index 0000000000..8ba78bc644 --- /dev/null +++ b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.13.1.mdx @@ -0,0 +1,312 @@ +import { LinkCard } from '@astrojs/starlight/components'; +import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro'; + +`enssdk` is the foundational TypeScript/JavaScript SDK for ENS development. It's a fully modular and tree-shakeable modern TypeScript/JavaScript package that provides ENS-specific types and helpers — usable from any JS runtime, browser or server. + +`enssdk` also directly integrates with ENSNode, providing an `EnsNodeClient` (via `createEnsNodeClient`) that can be extended with a fully-typed Omnigraph API client (powered by [`gql.tada`](https://gql-tada.0no.co/)). + +This guide walks you from an empty directory to a working TypeScript script that queries the `eth` Domain and queries its subdomains — the same flow as our [enssdk-example](https://github.com/namehash/ensnode/tree/main/examples/enssdk-example). + + + +## 1. Scaffold a TypeScript project + +If you already have a TypeScript project, skip ahead to [Install `enssdk`](#2-install-enssdk). + +Otherwise: + +```sh +mkdir my-ens-script && cd my-ens-script +npm init -y +mkdir src +``` + +## 2. Install `enssdk` + +We'll use [`tsx`](https://tsx.is/) to run TypeScript directly without a bundler. + +```sh +npm install enssdk@1.13.1 +npm install -D tsx typescript @types/node +``` + +:::caution[Pin exact versions] +Always pin an **exact** version (no `^` or `~`) of `enssdk`. The Omnigraph GraphQL schema is bundled inside `enssdk` and consumed by the `gql.tada` TypeScript plugin to type your queries — a minor or patch bump can change the schema and silently drift your generated types away from your queries. Locking the exact version keeps types and runtime in sync, and matched to the ENSNode version your hosted instance runs. +::: + + +## 3. Configure the `gql.tada` TypeScript plugin + +`gql.tada` is what gives your `graphql(...)` query strings end-to-end type safety. It reads the Omnigraph schema from `enssdk` at typecheck time. + +Create `tsconfig.json`: + +```json title="tsconfig.json" ins={10-15} +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "lib": ["ESNext"], + "types": ["node"], + "plugins": [ + { + "name": "gql.tada/ts-plugin", + "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql", + "tadaOutputLocation": "./src/generated/graphql-env.d.ts" + } + ] + }, + "include": ["src"] +} +``` + +If you're using VS Code, make sure your workspace is using the workspace TypeScript version so the plugin loads. Add this to `.vscode/settings.json`: + +```json title=".vscode/settings.json" +{ + "js/ts.tsdk.path": "node_modules/typescript/lib", + "js/ts.tsdk.promptToUseWorkspaceVersion": true +} +``` + +Also add a `start` script to `package.json`: + +```json title="package.json" ins={3-5} +{ + "type": "module", + "scripts": { + "start": "tsx src/index.ts" + } +} +``` + +## 4. Construct the client + +The `EnsNodeClient` is the entry point. Extend it with the `omnigraph` module to get the `client.omnigraph.query(...)` method. + +```ts title="src/index.ts" +import { createEnsNodeClient } from "enssdk/core"; +import { omnigraph } from "enssdk/omnigraph"; + +// you may use a NameHash Hosted ENSNode instance +// learn more at https://ensnode.io/docs/hosted-instances +const ENSNODE_URL = process.env.ENSNODE_URL!; + +// create and extend an EnsNodeClient with Omnigraph support +const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph); +``` + +## 5. Hello world + +Add your first query — look up the `eth` Domain and print its owner and protocol version. + +:::tip[InterpretedName] +An **InterpretedName** is a Name whose labels are each either normalized or represented as an [encoded labelhash](/docs/reference/terminology#encoded-labelhash) (e.g. `[abcd...].eth`) — the canonical, lossless form ENSNode uses to identify a Name. `asInterpretedName("eth")` brands a known-safe string as one; for user input, validate first. See [Interpreted Name](/docs/reference/terminology#interpreted-name) in the terminology reference. +::: + +```ts title="src/index.ts" +// existing imports... + +import { asInterpretedName, beautifyInterpretedName } from "enssdk"; +import { graphql, omnigraph } from "enssdk/omnigraph"; + +// existing client... + +// this is typechecked and editor autocompleted with built-in docs! +const HelloWorldQuery = graphql(` + query HelloWorld($name: InterpretedName!) { + domain(by: { name: $name }) { + __typename + name + owner { address } + } + } +`); + +async function main() { + const name = asInterpretedName("eth"); + + const result = await client.omnigraph.query({ + query: HelloWorldQuery, + variables: { name }, + }); + + if (result.errors) throw new Error(JSON.stringify(result.errors)); + if (!result.data?.domain) throw new Error(`Domain '${name}' not found`); + + const { domain } = result.data; + + console.log(`Name: ${domain.name ? beautifyInterpretedName(domain.name) : ""}`); + console.log(`Version: ${domain.__typename}`); + console.log(`Owner: ${domain.owner?.address ?? "0x0"}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + +A few things to notice: + +- `graphql(...)` parses your query at typecheck time. Hover over `result.data` and you'll see it's typed exactly to your selection set — try removing `owner { address }` from the query and watch the access below become a type error. +- `domain` is a union of `ENSv1Domain | ENSv2Domain` (both implement the `Domain` interface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query — `__typename` tells you which one you got. +- `name` is `null` for non-canonical Domains (e.g. Domains whose name cannot be inferred). When non-null, it is the Domain's name as an InterpretedName. **Always** guard the access; TypeScript will help you. + +## 6. List subdomains + +Expand the query to also fetch the Domain's subdomains. `subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm) — pass `first: 20` to cap the page, and select `totalCount` to learn how many subdomains exist in total. + +```ts title="src/index.ts" ins={7-12,34-38} +const HelloWorldQuery = graphql(` + query HelloWorld($name: InterpretedName!) { + domain(by: { name: $name }) { + __typename + name + owner { address } + subdomains(first: 20) { + totalCount + edges { + node { name owner { address } } + } + } + } + } +`); + +async function main() { + const name = asInterpretedName("eth"); + + const result = await client.omnigraph.query({ + query: HelloWorldQuery, + variables: { name }, + }); + + if (result.errors) throw new Error(JSON.stringify(result.errors)); + if (!result.data?.domain) throw new Error(`Domain '${name}' not found`); + + const { domain } = result.data; + + console.log(`Name: ${domain.name ? beautifyInterpretedName(domain.name) : ""}`); + console.log(`Version: ${domain.__typename}`); + console.log(`Owner: ${domain.owner?.address ?? "0x0"}`); + + console.log(`\nSubdomains (showing 20 of ${domain.subdomains?.totalCount ?? 0}):`); + for (const { node } of domain.subdomains?.edges ?? []) { + const subName = node.name ? beautifyInterpretedName(node.name) : ""; + console.log(` - ${subName} — Owner ${node.owner?.address ?? "0x0"}`); + } +} +``` + +Notice we're now writing the same name/owner rendering twice — once for the parent Domain and once inside the subdomain loop. We'll fix that next. + +To page beyond the first 20, the connection also exposes `pageInfo { hasNextPage endCursor }` and accepts an `after: String` cursor — see [Relay's connection spec](https://relay.dev/graphql/connections.htm). + +## 7. Extract a typed fragment + +Notice we're selecting the same fields (`name`, `owner { address }`) on the parent Domain _and_ on each subdomain, and rendering them the same way. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed function that formats a Domain. + +```ts title="src/index.ts" ins={3,11-17,23,26,31,34-40,56,59} +import { asInterpretedName, beautifyInterpretedName } from "enssdk"; +import { createEnsNodeClient } from "enssdk/core"; +import { type FragmentOf, graphql, omnigraph, readFragment } from "enssdk/omnigraph"; + +// you may use a NameHash Hosted ENSNode instance +// learn more at https://ensnode.io/docs/hosted-instances +const ENSNODE_URL = process.env.ENSNODE_URL!; + +const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph); + +const DomainFragment = graphql(` + fragment DomainFragment on Domain { + __typename + name + owner { address } + } +`); + +const HelloWorldQuery = graphql( + ` + query HelloWorld($name: InterpretedName!) { + domain(by: { name: $name }) { + ...DomainFragment + subdomains(first: 20) { + totalCount + edges { node { ...DomainFragment } } + } + } + } +`, + [DomainFragment], +); + +function formatDomain(data: FragmentOf): string { + // type-safe access to fragment data! + const domain = readFragment(DomainFragment, data); + const name = domain.name ? beautifyInterpretedName(domain.name) : ""; + const owner = domain.owner?.address ?? "0x0"; + return `${name} (${domain.__typename}) — Owner ${owner}`; +} + +async function main() { + const name = asInterpretedName("eth"); + + const result = await client.omnigraph.query({ + query: HelloWorldQuery, + variables: { name }, + }); + + if (result.errors) throw new Error(JSON.stringify(result.errors)); + if (!result.data?.domain) throw new Error(`Domain '${name}' not found`); + + const { domain } = result.data; + const totalCount = domain.subdomains?.totalCount ?? 0; + + console.log(formatDomain(domain)); + console.log(`\nSubdomains (showing 20 of ${totalCount}):`); + for (const { node } of domain.subdomains?.edges ?? []) { + console.log(` - ${formatDomain(node)}`); + } +} +``` + +`FragmentOf` is the opaque type for any selection that includes `...DomainFragment` — `formatDomain` accepts any of them, including each `node` in the subdomain edges. `readFragment(DomainFragment, data)` unwraps that opaque type to the typed fields you declared. + +## 8. Run it + +Point at a hosted ENSNode and go: + +```sh +ENSNODE_URL=https://api.v2-sepolia.ensnode.io npm start +``` + +You should see the `eth` Domain, followed by its first 20 subdomains and the total subdomain count. + +## Where to go next + +- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy GraphQL queries: account-owned domains, events, registrar permissions, full-text search, and more. +- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query. +- Building a React app? Use [`enskit`](/docs/integrate/integration-options/enskit) — same `graphql(...)` helper, with `useOmnigraphQuery` and a graphcache. + + + + + + diff --git a/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx new file mode 100644 index 0000000000..100a42c207 --- /dev/null +++ b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx @@ -0,0 +1,312 @@ +import { LinkCard } from '@astrojs/starlight/components'; +import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro'; + +`enssdk` is the foundational TypeScript/JavaScript SDK for ENS development. It's a fully modular and tree-shakeable modern TypeScript/JavaScript package that provides ENS-specific types and helpers — usable from any JS runtime, browser or server. + +`enssdk` also directly integrates with ENSNode, providing an `EnsNodeClient` (via `createEnsNodeClient`) that can be extended with a fully-typed Omnigraph API client (powered by [`gql.tada`](https://gql-tada.0no.co/)). + +This guide walks you from an empty directory to a working TypeScript script that queries the `eth` Domain and queries its subdomains — the same flow as our [enssdk-example](https://github.com/namehash/ensnode/tree/main/examples/enssdk-example). + + + +## 1. Scaffold a TypeScript project + +If you already have a TypeScript project, skip ahead to [Install `enssdk`](#2-install-enssdk). + +Otherwise: + +```sh +mkdir my-ens-script && cd my-ens-script +npm init -y +mkdir src +``` + +## 2. Install `enssdk` + +We'll use [`tsx`](https://tsx.is/) to run TypeScript directly without a bundler. + +```sh +npm install enssdk@1.14.1 +npm install -D tsx typescript @types/node +``` + +:::caution[Pin exact versions] +Always pin an **exact** version (no `^` or `~`) of `enssdk`. The Omnigraph GraphQL schema is bundled inside `enssdk` and consumed by the `gql.tada` TypeScript plugin to type your queries — a minor or patch bump can change the schema and silently drift your generated types away from your queries. Locking the exact version keeps types and runtime in sync, and matched to the ENSNode version your hosted instance runs. +::: + + +## 3. Configure the `gql.tada` TypeScript plugin + +`gql.tada` is what gives your `graphql(...)` query strings end-to-end type safety. It reads the Omnigraph schema from `enssdk` at typecheck time. + +Create `tsconfig.json`: + +```json title="tsconfig.json" ins={10-15} +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "lib": ["ESNext"], + "types": ["node"], + "plugins": [ + { + "name": "gql.tada/ts-plugin", + "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql", + "tadaOutputLocation": "./src/generated/graphql-env.d.ts" + } + ] + }, + "include": ["src"] +} +``` + +If you're using VS Code, make sure your workspace is using the workspace TypeScript version so the plugin loads. Add this to `.vscode/settings.json`: + +```json title=".vscode/settings.json" +{ + "js/ts.tsdk.path": "node_modules/typescript/lib", + "js/ts.tsdk.promptToUseWorkspaceVersion": true +} +``` + +Also add a `start` script to `package.json`: + +```json title="package.json" ins={3-5} +{ + "type": "module", + "scripts": { + "start": "tsx src/index.ts" + } +} +``` + +## 4. Construct the client + +The `EnsNodeClient` is the entry point. Extend it with the `omnigraph` module to get the `client.omnigraph.query(...)` method. + +```ts title="src/index.ts" +import { createEnsNodeClient } from "enssdk/core"; +import { omnigraph } from "enssdk/omnigraph"; + +// you may use a NameHash Hosted ENSNode instance +// learn more at https://ensnode.io/docs/hosted-instances +const ENSNODE_URL = process.env.ENSNODE_URL!; + +// create and extend an EnsNodeClient with Omnigraph support +const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph); +``` + +## 5. Hello world + +Add your first query — look up the `eth` Domain and print its owner and protocol version. + +:::tip[InterpretedName] +An **InterpretedName** is a Name whose labels are each either normalized or represented as an [encoded labelhash](/docs/reference/terminology#encoded-labelhash) (e.g. `[abcd...].eth`) — the canonical, lossless form ENSNode uses to identify a Name. `asInterpretedName("eth")` brands a known-safe string as one; for user input, validate first. See [Interpreted Name](/docs/reference/terminology#interpreted-name) in the terminology reference. +::: + +```ts title="src/index.ts" +// existing imports... + +import { asInterpretedName, beautifyInterpretedName } from "enssdk"; +import { graphql, omnigraph } from "enssdk/omnigraph"; + +// existing client... + +// this is typechecked and editor autocompleted with built-in docs! +const HelloWorldQuery = graphql(` + query HelloWorld($name: InterpretedName!) { + domain(by: { name: $name }) { + __typename + canonical { name { interpreted } } + owner { address } + } + } +`); + +async function main() { + const name = asInterpretedName("eth"); + + const result = await client.omnigraph.query({ + query: HelloWorldQuery, + variables: { name }, + }); + + if (result.errors) throw new Error(JSON.stringify(result.errors)); + if (!result.data?.domain) throw new Error(`Domain '${name}' not found`); + + const { domain } = result.data; + + console.log(`Name: ${domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : ""}`); + console.log(`Version: ${domain.__typename}`); + console.log(`Owner: ${domain.owner?.address ?? "0x0"}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + +A few things to notice: + +- `graphql(...)` parses your query at typecheck time. Hover over `result.data` and you'll see it's typed exactly to your selection set — try removing `owner { address }` from the query and watch the access below become a type error. +- `domain` is a union of `ENSv1Domain | ENSv2Domain` (both implement the `Domain` interface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query — `__typename` tells you which one you got. +- `canonical` is `null` for non-canonical Domains (e.g. Domains whose name cannot be inferred). When non-null, `canonical.name.interpreted` is the Domain's Canonical Name as an InterpretedName. **Always** guard the access; TypeScript will help you. + +## 6. List subdomains + +Expand the query to also fetch the Domain's subdomains. `subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm) — pass `first: 20` to cap the page, and select `totalCount` to learn how many subdomains exist in total. + +```ts title="src/index.ts" ins={7-12,34-38} +const HelloWorldQuery = graphql(` + query HelloWorld($name: InterpretedName!) { + domain(by: { name: $name }) { + __typename + canonical { name { interpreted } } + owner { address } + subdomains(first: 20) { + totalCount + edges { + node { canonical { name { interpreted } } owner { address } } + } + } + } + } +`); + +async function main() { + const name = asInterpretedName("eth"); + + const result = await client.omnigraph.query({ + query: HelloWorldQuery, + variables: { name }, + }); + + if (result.errors) throw new Error(JSON.stringify(result.errors)); + if (!result.data?.domain) throw new Error(`Domain '${name}' not found`); + + const { domain } = result.data; + + console.log(`Name: ${domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : ""}`); + console.log(`Version: ${domain.__typename}`); + console.log(`Owner: ${domain.owner?.address ?? "0x0"}`); + + console.log(`\nSubdomains (showing 20 of ${domain.subdomains?.totalCount ?? 0}):`); + for (const { node } of domain.subdomains?.edges ?? []) { + const subName = node.canonical ? beautifyInterpretedName(node.canonical.name.interpreted) : ""; + console.log(` - ${subName} — Owner ${node.owner?.address ?? "0x0"}`); + } +} +``` + +Notice we're now writing the same canonical/owner rendering twice — once for the parent Domain and once inside the subdomain loop. We'll fix that next. + +To page beyond the first 20, the connection also exposes `pageInfo { hasNextPage endCursor }` and accepts an `after: String` cursor — see [Relay's connection spec](https://relay.dev/graphql/connections.htm). + +## 7. Extract a typed fragment + +Notice we're selecting the same fields (`canonical { name { interpreted } }`, `owner { address }`) on the parent Domain _and_ on each subdomain, and rendering them the same way. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed function that formats a Domain. + +```ts title="src/index.ts" ins={3,11-17,23,26,31,34-40,56,59} +import { asInterpretedName, beautifyInterpretedName } from "enssdk"; +import { createEnsNodeClient } from "enssdk/core"; +import { type FragmentOf, graphql, omnigraph, readFragment } from "enssdk/omnigraph"; + +// you may use a NameHash Hosted ENSNode instance +// learn more at https://ensnode.io/docs/hosted-instances +const ENSNODE_URL = process.env.ENSNODE_URL!; + +const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph); + +const DomainFragment = graphql(` + fragment DomainFragment on Domain { + __typename + canonical { name { interpreted } } + owner { address } + } +`); + +const HelloWorldQuery = graphql( + ` + query HelloWorld($name: InterpretedName!) { + domain(by: { name: $name }) { + ...DomainFragment + subdomains(first: 20) { + totalCount + edges { node { ...DomainFragment } } + } + } + } +`, + [DomainFragment], +); + +function formatDomain(data: FragmentOf): string { + // type-safe access to fragment data! + const domain = readFragment(DomainFragment, data); + const name = domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : ""; + const owner = domain.owner?.address ?? "0x0"; + return `${name} (${domain.__typename}) — Owner ${owner}`; +} + +async function main() { + const name = asInterpretedName("eth"); + + const result = await client.omnigraph.query({ + query: HelloWorldQuery, + variables: { name }, + }); + + if (result.errors) throw new Error(JSON.stringify(result.errors)); + if (!result.data?.domain) throw new Error(`Domain '${name}' not found`); + + const { domain } = result.data; + const totalCount = domain.subdomains?.totalCount ?? 0; + + console.log(formatDomain(domain)); + console.log(`\nSubdomains (showing 20 of ${totalCount}):`); + for (const { node } of domain.subdomains?.edges ?? []) { + console.log(` - ${formatDomain(node)}`); + } +} +``` + +`FragmentOf` is the opaque type for any selection that includes `...DomainFragment` — `formatDomain` accepts any of them, including each `node` in the subdomain edges. `readFragment(DomainFragment, data)` unwraps that opaque type to the typed fields you declared. + +## 8. Run it + +Point at a hosted ENSNode and go: + +```sh +ENSNODE_URL=https://api.v2-sepolia.blue.ensnode.io npm start +``` + +You should see the `eth` Domain, followed by its first 20 subdomains and the total subdomain count. + +## Where to go next + +- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy GraphQL queries: account-owned domains, events, registrar permissions, full-text search, and more. +- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query. +- Building a React app? Use [`enskit`](/docs/integrate/integration-options/enskit) — same `graphql(...)` helper, with `useOmnigraphQuery` and a graphcache. + + + + + + diff --git a/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.13.1.mdx b/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.13.1.mdx new file mode 100644 index 0000000000..1527e7509a --- /dev/null +++ b/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.13.1.mdx @@ -0,0 +1,182 @@ +import { LinkCard } from '@astrojs/starlight/components'; +import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro'; + +The Omnigraph is **a GraphQL API** following the [Relay specification](https://relay.dev/graphql/connections.htm). There's no proprietary protocol or transport — any GraphQL client in any language works, from `curl` and `fetch` to [`urql`](https://nearform.com/open-source/urql/), [`Apollo`](https://www.apollographql.com/), [`graphql-request`](https://github.com/jasonkuhrt/graphql-request), and beyond. + +This guide walks you through the minimum: a single `fetch` call against `/api/omnigraph` — the same flow as our [omnigraph-graphql-example](https://github.com/namehash/ensnode/tree/main/examples/omnigraph-graphql-example). + +If you want end-to-end typed queries (via [`gql.tada`](https://gql-tada.0no.co/)) with editor autocomplete and a built-in client, use [`enssdk`](/docs/integrate/integration-options/enssdk) instead — but if you need to integrate from a language without first-class GraphQL tooling, or you're already in a stack with its own GraphQL client, this is the path. + + + +## 1. The endpoint + +The Omnigraph lives at: + +``` +POST {ENSNODE_URL}/api/omnigraph +Content-Type: application/json + +{ "query": "...", "variables": { ... } } +``` + +It returns `{ "data": ..., "errors": [...] }` per the standard GraphQL response shape. + +A minimum-viable hello world over `curl`: + +```sh +curl -sS -X POST \ + -H 'Content-Type: application/json' \ + -d '{"query":"{ domain(by: { name: \"eth\" }) { name owner { address } } }"}' \ + https://api.v2-sepolia.ensnode.io/api/omnigraph +``` + +The rest of this guide builds the same thing in TypeScript using `fetch`, so you have something to extend. + +## 2. Scaffold a TypeScript project + +If you already have one, skip ahead to [Write the query](#3-write-the-query). + +```sh +mkdir my-ens-script && cd my-ens-script +npm init -y +mkdir src +``` + +Install [`tsx`](https://tsx.is/) so you can run TypeScript directly: + +```sh +npm install -D tsx typescript @types/node +``` + +Add a `start` script to `package.json`: + +```json title="package.json" ins={3-5} +{ + "type": "module", + "scripts": { + "start": "tsx src/index.ts" + } +} +``` + +## 3. Write the query + +Create `src/index.ts`. The whole script is a single `fetch` against `/api/omnigraph`. + +```ts title="src/index.ts" +// you may use a NameHash Hosted ENSNode instance +// learn more at https://ensnode.io/docs/hosted-instances +const ENSNODE_URL = process.env.ENSNODE_URL!; + +const HELLO_WORLD_QUERY = /* GraphQL */ ` + query HelloWorld($name: InterpretedName!) { + domain(by: { name: $name }) { + __typename + name + owner { address } + subdomains(first: 20) { + totalCount + edges { node { __typename name owner { address } } } + } + } + } +`; + +interface Domain { + __typename: "ENSv1Domain" | "ENSv2Domain"; + name: string | null; + owner: { address: string } | null; +} + +interface QueryResult { + data?: { + domain: (Domain & { + subdomains: { + totalCount: number; + edges: { node: Domain }[]; + } | null; + }) | null; + } | null; + errors?: { message: string }[]; +} + +function formatDomain(domain: Domain): string { + const name = domain.name ?? ""; + const owner = domain.owner?.address ?? "0x0"; + return `${name} (${domain.__typename}) — Owner ${owner}`; +} + +async function main() { + const response = await fetch(new URL("/api/omnigraph", ENSNODE_URL), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: HELLO_WORLD_QUERY, + variables: { name: "eth" }, + }), + }); + + if (!response.ok) { + throw new Error(`Request failed: ${response.status} ${response.statusText}`); + } + + const { data, errors } = (await response.json()) as QueryResult; + + if (errors) throw new Error(JSON.stringify(errors)); + if (!data?.domain) throw new Error("Domain 'eth' not found"); + + const { domain } = data; + const totalCount = domain.subdomains?.totalCount ?? 0; + + console.log(formatDomain(domain)); + console.log(`\nSubdomains (showing 20 of ${totalCount}):`); + for (const { node } of domain.subdomains?.edges ?? []) { + console.log(` - ${formatDomain(node)}`); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + +A few things to notice: + +- **`InterpretedName` is a scalar.** From the wire's perspective it's just a string — the server validates the format. Pass `"eth"` as a plain string in `variables`. +- **`subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm).** Cursor through with `first`, `after`, `pageInfo { hasNextPage endCursor }` — same shape as any Relay-style API. +- **Hand-written types.** We're maintaining `interface Domain` and `interface QueryResult` ourselves here. If you want these generated and kept in sync with your queries automatically, use [`enssdk`](/docs/integrate/integration-options/enssdk). + +## 4. Run it + +```sh +ENSNODE_URL=https://api.v2-sepolia.ensnode.io npm start +``` + +You should see the `eth` Domain, its owner, and the first 20 of its subdomains. + +## Where to go next + +- Want typed queries with editor autocomplete and a real GraphQL client? Use [`enssdk`](/docs/integrate/integration-options/enssdk) — same API, with `gql.tada` types and an `EnsNodeClient`. +- Building a React app? Use [`enskit`](/docs/integrate/integration-options/enskit) — same `graphql(...)` helper plus `useOmnigraphQuery` and a graphcache. +- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more. +- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query. + + + + + + diff --git a/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx new file mode 100644 index 0000000000..7b46a1d861 --- /dev/null +++ b/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx @@ -0,0 +1,182 @@ +import { LinkCard } from '@astrojs/starlight/components'; +import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro'; + +The Omnigraph is **a GraphQL API** following the [Relay specification](https://relay.dev/graphql/connections.htm). There's no proprietary protocol or transport — any GraphQL client in any language works, from `curl` and `fetch` to [`urql`](https://nearform.com/open-source/urql/), [`Apollo`](https://www.apollographql.com/), [`graphql-request`](https://github.com/jasonkuhrt/graphql-request), and beyond. + +This guide walks you through the minimum: a single `fetch` call against `/api/omnigraph` — the same flow as our [omnigraph-graphql-example](https://github.com/namehash/ensnode/tree/main/examples/omnigraph-graphql-example). + +If you want end-to-end typed queries (via [`gql.tada`](https://gql-tada.0no.co/)) with editor autocomplete and a built-in client, use [`enssdk`](/docs/integrate/integration-options/enssdk) instead — but if you need to integrate from a language without first-class GraphQL tooling, or you're already in a stack with its own GraphQL client, this is the path. + + + +## 1. The endpoint + +The Omnigraph lives at: + +``` +POST {ENSNODE_URL}/api/omnigraph +Content-Type: application/json + +{ "query": "...", "variables": { ... } } +``` + +It returns `{ "data": ..., "errors": [...] }` per the standard GraphQL response shape. + +A minimum-viable hello world over `curl`: + +```sh +curl -sS -X POST \ + -H 'Content-Type: application/json' \ + -d '{"query":"{ domain(by: { name: \"eth\" }) { canonical { name { interpreted } } owner { address } } }"}' \ + https://api.v2-sepolia.blue.ensnode.io/api/omnigraph +``` + +The rest of this guide builds the same thing in TypeScript using `fetch`, so you have something to extend. + +## 2. Scaffold a TypeScript project + +If you already have one, skip ahead to [Write the query](#3-write-the-query). + +```sh +mkdir my-ens-script && cd my-ens-script +npm init -y +mkdir src +``` + +Install [`tsx`](https://tsx.is/) so you can run TypeScript directly: + +```sh +npm install -D tsx typescript @types/node +``` + +Add a `start` script to `package.json`: + +```json title="package.json" ins={3-5} +{ + "type": "module", + "scripts": { + "start": "tsx src/index.ts" + } +} +``` + +## 3. Write the query + +Create `src/index.ts`. The whole script is a single `fetch` against `/api/omnigraph`. + +```ts title="src/index.ts" +// you may use a NameHash Hosted ENSNode instance +// learn more at https://ensnode.io/docs/hosted-instances +const ENSNODE_URL = process.env.ENSNODE_URL!; + +const HELLO_WORLD_QUERY = /* GraphQL */ ` + query HelloWorld($name: InterpretedName!) { + domain(by: { name: $name }) { + __typename + canonical { name { interpreted } } + owner { address } + subdomains(first: 20) { + totalCount + edges { node { __typename canonical { name { interpreted } } owner { address } } } + } + } + } +`; + +interface Domain { + __typename: "ENSv1Domain" | "ENSv2Domain"; + canonical: { name: { interpreted: string } } | null; + owner: { address: string } | null; +} + +interface QueryResult { + data?: { + domain: (Domain & { + subdomains: { + totalCount: number; + edges: { node: Domain }[]; + } | null; + }) | null; + } | null; + errors?: { message: string }[]; +} + +function formatDomain(domain: Domain): string { + const name = domain.canonical?.name.interpreted ?? ""; + const owner = domain.owner?.address ?? "0x0"; + return `${name} (${domain.__typename}) — Owner ${owner}`; +} + +async function main() { + const response = await fetch(new URL("/api/omnigraph", ENSNODE_URL), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: HELLO_WORLD_QUERY, + variables: { name: "eth" }, + }), + }); + + if (!response.ok) { + throw new Error(`Request failed: ${response.status} ${response.statusText}`); + } + + const { data, errors } = (await response.json()) as QueryResult; + + if (errors) throw new Error(JSON.stringify(errors)); + if (!data?.domain) throw new Error("Domain 'eth' not found"); + + const { domain } = data; + const totalCount = domain.subdomains?.totalCount ?? 0; + + console.log(formatDomain(domain)); + console.log(`\nSubdomains (showing 20 of ${totalCount}):`); + for (const { node } of domain.subdomains?.edges ?? []) { + console.log(` - ${formatDomain(node)}`); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + +A few things to notice: + +- **`InterpretedName` is a scalar.** From the wire's perspective it's just a string — the server validates the format. Pass `"eth"` as a plain string in `variables`. +- **`subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm).** Cursor through with `first`, `after`, `pageInfo { hasNextPage endCursor }` — same shape as any Relay-style API. +- **Hand-written types.** We're maintaining `interface Domain` and `interface QueryResult` ourselves here. If you want these generated and kept in sync with your queries automatically, use [`enssdk`](/docs/integrate/integration-options/enssdk). + +## 4. Run it + +```sh +ENSNODE_URL=https://api.v2-sepolia.blue.ensnode.io npm start +``` + +You should see the `eth` Domain, its owner, and the first 20 of its subdomains. + +## Where to go next + +- Want typed queries with editor autocomplete and a real GraphQL client? Use [`enssdk`](/docs/integrate/integration-options/enssdk) — same API, with `gql.tada` types and an `EnsNodeClient`. +- Building a React app? Use [`enskit`](/docs/integrate/integration-options/enskit) — same `graphql(...)` helper plus `useOmnigraphQuery` and a graphcache. +- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more. +- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query. + + + + + + diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx index 60140297c9..c4233ff242 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx @@ -3,417 +3,14 @@ title: enskit description: React toolkit for ENSv2 development, includes fully typed providers for the ENS Omnigraph API. --- -import { LinkCard, Steps } from '@astrojs/starlight/components'; -import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro'; +import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active"; +import WalkthroughV1131 from "@components/walkthroughs/enskit/v1.13.1.mdx"; +import WalkthroughV1141 from "@components/walkthroughs/enskit/v1.14.1.mdx"; -`enskit` is the React toolkit for ENSv2 development. It provides a fully typed Omnigraph API client (powered by [`urql`](https://nearform.com/open-source/urql/) and [`gql.tada`](https://gql-tada.0no.co/)), the `OmnigraphProvider`, and the `useOmnigraphQuery` hook for writing type-safe ENS queries with editor autocomplete, Relay-style pagination, and Omnigraph-specific cache directives. +{/* + Version-locked to the production-deployed Omnigraph schema (see `@data/omnigraph-examples/active`). + Author one partial per supported version under `@components/walkthroughs/enskit/` and render the + active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION; no edits here are needed. +*/} -This guide walks you from an empty directory to a working React component that renders an [ENS Domain](/docs/concepts/the-ens-protocol) and a paginated list of its subdomains — the same flow as the [`DomainView`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in our example app. - - - - -## 1. Scaffold a React app - -If you already have a React + TypeScript app, skip ahead to [Install `enskit` and `enssdk`](#2-install-enskit-and-enssdk). - -Otherwise, the fastest way to get going is [Vite](https://vite.dev): - -```sh -npm create vite@latest my-ens-app -- --template react-ts -cd my-ens-app -npm install -``` - -## 2. Install `enskit` and `enssdk` - -```sh -npm install enskit@1.13.1 enssdk@1.13.1 -``` - -:::caution[Pin exact versions — hosted instance compatibility] -Always pin **exact** versions (no `^` or `~`) of `enskit` and `enssdk`, and keep them on the same version. The Omnigraph GraphQL schema is bundled inside `enssdk` and consumed by the `gql.tada` TypeScript plugin to type your queries — a minor or patch bump can change the schema and silently drift your generated types away from your queries. Locking exact versions keeps types and runtime in sync. - -**Important:** Our hosted ENSNode instances currently run ENSNode v1.13. The latest published versions of `enskit` and `enssdk` are `1.14.0+`, which contain breaking changes in the Omnigraph API data model not yet deployed to our hosted infrastructure. If you are querying a hosted instance from your own app, you **must** use `enskit@1.13.1` and `enssdk@1.13.1` to avoid type errors and runtime mismatches. This notice will be removed once the hosted instances are upgraded. -::: - -## 3. Configure the `gql.tada` TypeScript plugin - -`gql.tada` is what gives your `graphql(...)` query strings end-to-end type safety. It reads the Omnigraph schema from `enssdk` at typecheck time. - -Add the plugin to `tsconfig.json`: - -```json title="tsconfig.json" {6-13} -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "plugins": [ - { - "name": "gql.tada/ts-plugin", - "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql", - "tadaOutputLocation": "./src/generated/graphql-env.d.ts" - } - ] - }, - "include": ["src"] -} -``` - -If you're using VS Code, make sure your workspace is using the workspace TypeScript version so the plugin loads. Add this to `.vscode/settings.json`: - -```json title=".vscode/settings.json" -{ - "js/ts.tsdk.path": "node_modules/typescript/lib", - "js/ts.tsdk.promptToUseWorkspaceVersion": true -} -``` - -## 4. Mount the `OmnigraphProvider` - -`OmnigraphProvider` is what `useOmnigraphQuery` reads from. Construct an `EnsNodeClient`, extend it with the `omnigraph` module, and wrap your app: - -```tsx title="src/App.tsx" -import { OmnigraphProvider } from "enskit/react/omnigraph"; -import { createEnsNodeClient } from "enssdk/core"; -import { omnigraph } from "enssdk/omnigraph"; -import { StrictMode } from "react"; - -import { DomainView } from "./DomainView"; - -// you may use a NameHash Hosted ENSNode instance -// learn more at https://ensnode.io/docs/hosted-instances -const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL! - -// create and extend an EnsNodeClient with Omnigraph support -const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph); - -export function App() { - return ( - - -

        My ENS App

        - -
        -
        - ); -} -``` - -## 5. Hello world - -Create `src/DomainView.tsx`. We'll start with the simplest possible query — look up the `eth` Domain and render its owner and protocol version. - -:::tip[InterpretedName] -An **InterpretedName** is a Name whose labels are each either normalized or represented as an [encoded labelhash](/docs/reference/terminology#encoded-labelhash) (e.g. `[abcd...].eth`) — the canonical, lossless form ENSNode uses to identify a Name. `asInterpretedName("eth")` brands a known-safe string as one; for user input, validate first. See [Interpreted Name](/docs/reference/terminology#interpreted-name) in the terminology reference. -::: - -```tsx title="src/DomainView.tsx" -import { graphql, useOmnigraphQuery } from "enskit/react/omnigraph"; -import { asInterpretedName, beautifyInterpretedName } from "enssdk"; - -const DomainByNameQuery = graphql(` - query DomainByName($name: InterpretedName!) { - domain(by: { name: $name }) { - __typename - canonical { name { interpreted } } - owner { address } - } - } -`); - -export function DomainView() { - const name = asInterpretedName("eth"); - - const [result] = useOmnigraphQuery({ - query: DomainByNameQuery, - variables: { name }, - }); - - const { data, fetching, error } = result; - - if (!data && fetching) return

        Loading...

        ; - if (error) return

        Error: {error.message}

        ; - if (!data?.domain) return

        No domain found.

        ; - - const { domain } = data; - - return ( -
        -

        - {domain.canonical - ? beautifyInterpretedName(domain.canonical.name.interpreted) - : "Unnamed Domain"} -

        -

        Version: {domain.__typename}

        -

        - Owner: {domain.owner?.address ?? "0x0"} -

        -
        - ); -} -``` - -A few things to notice: - -- `graphql(...)` parses your query at typecheck time. Hover over `result.data` and you'll see it's typed exactly to your selection set — try removing `owner { address }` from the query and watch the access below become a type error. -- `domain` is a union of `ENSv1Domain | ENSv2Domain` (both implement the `Domain` interface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query — `__typename` tells you which one you got. -- `canonical` may be `null` for non-canonical names (e.g. Domains whose name cannot be inferred). **Always** guard the access; TypeScript will help you. - -## 6. List subdomains - -Expand the query to also fetch the Domain's subdomains. `subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm), so the shape is `{ edges: [{ node }] }`. - -```tsx title="src/DomainView.tsx" ins={7-13,40-54} -const DomainByNameQuery = graphql(` - query DomainByName($name: InterpretedName!) { - domain(by: { name: $name }) { - __typename - canonical { name { interpreted } } - owner { address } - subdomains { - edges { - node { - canonical { name { interpreted } } - owner { address } - } - } - } - } - } -`); - -export function DomainView() { - const name = asInterpretedName("eth"); - - const [result] = useOmnigraphQuery({ - query: DomainByNameQuery, - variables: { name }, - }); - - const { data, fetching, error } = result; - - if (!data && fetching) return

        Loading...

        ; - if (error) return

        Error: {error.message}

        ; - if (!data?.domain) return

        No domain found.

        ; - - const { domain } = data; - - return ( -
        -

        {domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : "Unnamed Domain"}

        -

        Version: {domain.__typename}

        -

        Owner: {domain.owner?.address ?? "0x0"}

        - -

        Subdomains

        -
          - {domain.subdomains?.edges.map(({ node }, i) => ( -
        • - {node.canonical - ? beautifyInterpretedName(node.canonical.name.interpreted) - : unnamed}{" "} - — Owner {node.owner?.address ?? "0x0"} -
        • - ))} -
        -
        - ); -} -``` - -## 7. Extract a typed fragment - -Notice we're selecting the same fields (`canonical { name { interpreted } }`, `owner { address }`) on the parent Domain _and_ on each subdomain. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed shape for components that render a Domain. - -```tsx title="src/DomainView.tsx" ins={1,2,5,9-15,21,23,28,31-48,66,72} -import { - type FragmentOf, - graphql, - readFragment, - useOmnigraphQuery, -} from "enskit/react/omnigraph"; -import { asInterpretedName, beautifyInterpretedName } from "enssdk"; - -const DomainFragment = graphql(` - fragment DomainFragment on Domain { - __typename - canonical { name { interpreted } } - owner { address } - } -`); - -const DomainByNameQuery = graphql( - ` - query DomainByName($name: InterpretedName!) { - domain(by: { name: $name }) { - ...DomainFragment - subdomains { - edges { node { ...DomainFragment } } - } - } - } -`, - [DomainFragment], -); - -function RenderDomain({ data }: { data: FragmentOf }) { - // type-safe access to fragment data! - const domain = readFragment(DomainFragment, data); - - return ( - <> - - {domain.canonical - ? beautifyInterpretedName(domain.canonical.name.interpreted) - : "Unnamed Domain"} - {" "} - ({domain.__typename}){" "} - - — Owner {domain.owner?.address ?? "0x0"} - - - ); -} - -export function DomainView() { - const name = asInterpretedName("eth"); - - const [result] = useOmnigraphQuery({ - query: DomainByNameQuery, - variables: { name }, - }); - - const { data, fetching, error } = result; - - if (!data && fetching) return

        Loading...

        ; - if (error) return

        Error: {error.message}

        ; - if (!data?.domain) return

        No domain found.

        ; - - return ( -
        -

        - -

        Subdomains

        -
          - {data.domain.subdomains?.edges.map(({ node }, i) => ( -
        • - -
        • - ))} -
        -
        - ); -} -``` - -`FragmentOf` is the opaque type for any selection that includes `...DomainFragment` — `RenderDomain` accepts any of them. `readFragment(DomainFragment, data)` unwraps that opaque type to the typed fields you declared. - -## 8. Paginate with "Load more" - -`subdomains` is a Relay Connection — page through it with the `first` and `after` arguments. Add `pageInfo { hasNextPage endCursor }` to the query, track the cursor in component state, and wire up a "Next page" button. - -```tsx title="src/DomainView.tsx" ins={1,6,9,11,19,23,27,51-59} -import { useState } from "react"; -// ...other imports - -const DomainByNameQuery = graphql( - ` - query DomainByName($name: InterpretedName!, $first: Int!, $after: String) { - domain(by: { name: $name }) { - ...DomainFragment - subdomains(first: $first, after: $after) { - edges { node { ...DomainFragment } } - pageInfo { hasNextPage endCursor } - } - } - } -`, - [DomainFragment], -); - -const PAGE_SIZE = 20; - -export function DomainView() { - const name = asInterpretedName("eth"); - const [after, setAfter] = useState(null); - - const [result] = useOmnigraphQuery({ - query: DomainByNameQuery, - variables: { name, first: PAGE_SIZE, after }, - }); - - const { data, fetching, error } = result; - - if (!data && fetching) return

        Loading...

        ; - if (error) return

        Error: {error.message}

        ; - if (!data?.domain) return

        No domain found.

        ; - - const { subdomains } = data.domain; - - return ( -
        -

        - -

        Subdomains

        -
          - {subdomains?.edges.map(({ node }, i) => ( -
        • - -
        • - ))} -
        - - {subdomains?.pageInfo.hasNextPage && ( - - )} -
        - ); -} -``` - -## 9. Run it - -```sh -VITE_ENSNODE_URL=https://api.alpha.ensnode.io npm run dev -``` - -Open the printed URL and you should see the `eth` Domain, its owner, and the first page of its subdomains. Clicking **Next page** advances the cursor. - -## Where to go next - -- Try the [Interactive Example](/docs/integrate/integration-options/enskit/example): edit and run the full `enskit-react-example` app in your browser with a live preview. -- Swap the hardcoded `"eth"` for a name from props or a router — see [`EnsureInterpretedName`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in the example app for safe handling of user-provided names. -- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more. -- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query. -- Need data outside React? Use [`enssdk`](/docs/integrate/integration-options/enssdk) directly with the same `graphql(...)` helper. - - - - - - - - +{ACTIVE_OMNIGRAPH_VERSION === "v1.14.1" ? : } diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx index 090c2a749d..9595e97ae8 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx @@ -3,317 +3,14 @@ title: enssdk description: SDK for ENSv2 development in TypeScript/JavaScript. --- -import { LinkCard } from '@astrojs/starlight/components'; -import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro'; +import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active"; +import WalkthroughV1131 from "@components/walkthroughs/enssdk/v1.13.1.mdx"; +import WalkthroughV1141 from "@components/walkthroughs/enssdk/v1.14.1.mdx"; -`enssdk` is the foundational TypeScript/JavaScript SDK for ENS development. It's a fully modular and tree-shakeable modern TypeScript/JavaScript package that provides ENS-specific types and helpers — usable from any JS runtime, browser or server. +{/* + Version-locked to the production-deployed Omnigraph schema (see `@data/omnigraph-examples/active`). + Author one partial per supported version under `@components/walkthroughs/enssdk/` and render the + active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION; no edits here are needed. +*/} -`enssdk` also directly integrates with ENSNode, providing an `EnsNodeClient` (via `createEnsNodeClient`) that can be extended with a fully-typed Omnigraph API client (powered by [`gql.tada`](https://gql-tada.0no.co/)). - -This guide walks you from an empty directory to a working TypeScript script that queries the `eth` Domain and queries its subdomains — the same flow as our [enssdk-example](https://github.com/namehash/ensnode/tree/main/examples/enssdk-example). - - - -## 1. Scaffold a TypeScript project - -If you already have a TypeScript project, skip ahead to [Install `enssdk`](#2-install-enssdk). - -Otherwise: - -```sh -mkdir my-ens-script && cd my-ens-script -npm init -y -mkdir src -``` - -## 2. Install `enssdk` - -We'll use [`tsx`](https://tsx.is/) to run TypeScript directly without a bundler. - -```sh -npm install enssdk@1.13.1 -npm install -D tsx typescript @types/node -``` - -:::caution[Pin exact versions — hosted instance compatibility] -Always pin an **exact** version (no `^` or `~`) of `enssdk`. The Omnigraph GraphQL schema is bundled inside `enssdk` and consumed by the `gql.tada` TypeScript plugin to type your queries — a minor or patch bump can change the schema and silently drift your generated types away from your queries. Locking the exact version keeps types and runtime in sync. - -**Important:** Our hosted ENSNode instances currently run ENSNode v1.13. The latest published version of `enssdk` is `1.14.0+`, which contains breaking changes in the Omnigraph API data model not yet deployed to our hosted infrastructure. If you are querying a hosted instance from your own app, you **must** use `enssdk@1.13.1` to avoid type errors and runtime mismatches. This notice will be removed once the hosted instances are upgraded. -::: - - -## 3. Configure the `gql.tada` TypeScript plugin - -`gql.tada` is what gives your `graphql(...)` query strings end-to-end type safety. It reads the Omnigraph schema from `enssdk` at typecheck time. - -Create `tsconfig.json`: - -```json title="tsconfig.json" ins={10-15} -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "lib": ["ESNext"], - "types": ["node"], - "plugins": [ - { - "name": "gql.tada/ts-plugin", - "schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql", - "tadaOutputLocation": "./src/generated/graphql-env.d.ts" - } - ] - }, - "include": ["src"] -} -``` - -If you're using VS Code, make sure your workspace is using the workspace TypeScript version so the plugin loads. Add this to `.vscode/settings.json`: - -```json title=".vscode/settings.json" -{ - "js/ts.tsdk.path": "node_modules/typescript/lib", - "js/ts.tsdk.promptToUseWorkspaceVersion": true -} -``` - -Also add a `start` script to `package.json`: - -```json title="package.json" ins={3-5} -{ - "type": "module", - "scripts": { - "start": "tsx src/index.ts" - } -} -``` - -## 4. Construct the client - -The `EnsNodeClient` is the entry point. Extend it with the `omnigraph` module to get the `client.omnigraph.query(...)` method. - -```ts title="src/index.ts" -import { createEnsNodeClient } from "enssdk/core"; -import { omnigraph } from "enssdk/omnigraph"; - -// you may use a NameHash Hosted ENSNode instance -// learn more at https://ensnode.io/docs/hosted-instances -const ENSNODE_URL = process.env.ENSNODE_URL!; - -// create and extend an EnsNodeClient with Omnigraph support -const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph); -``` - -## 5. Hello world - -Add your first query — look up the `eth` Domain and print its owner and protocol version. - -:::tip[InterpretedName] -An **InterpretedName** is a Name whose labels are each either normalized or represented as an [encoded labelhash](/docs/reference/terminology#encoded-labelhash) (e.g. `[abcd...].eth`) — the canonical, lossless form ENSNode uses to identify a Name. `asInterpretedName("eth")` brands a known-safe string as one; for user input, validate first. See [Interpreted Name](/docs/reference/terminology#interpreted-name) in the terminology reference. -::: - -```ts title="src/index.ts" -// existing imports... - -import { asInterpretedName, beautifyInterpretedName } from "enssdk"; -import { graphql, omnigraph } from "enssdk/omnigraph"; - -// existing client... - -// this is typechecked and editor autocompleted with built-in docs! -const HelloWorldQuery = graphql(` - query HelloWorld($name: InterpretedName!) { - domain(by: { name: $name }) { - __typename - canonical { name { interpreted } } - owner { address } - } - } -`); - -async function main() { - const name = asInterpretedName("eth"); - - const result = await client.omnigraph.query({ - query: HelloWorldQuery, - variables: { name }, - }); - - if (result.errors) throw new Error(JSON.stringify(result.errors)); - if (!result.data?.domain) throw new Error(`Domain '${name}' not found`); - - const { domain } = result.data; - - console.log(`Name: ${domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : ""}`); - console.log(`Version: ${domain.__typename}`); - console.log(`Owner: ${domain.owner?.address ?? "0x0"}`); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); -``` - -A few things to notice: - -- `graphql(...)` parses your query at typecheck time. Hover over `result.data` and you'll see it's typed exactly to your selection set — try removing `owner { address }` from the query and watch the access below become a type error. -- `domain` is a union of `ENSv1Domain | ENSv2Domain` (both implement the `Domain` interface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query — `__typename` tells you which one you got. -- `canonical` is `null` for non-canonical Domains (e.g. Domains whose name cannot be inferred). When non-null, `canonical.name.interpreted` is the Domain's Canonical Name as an InterpretedName. **Always** guard the access; TypeScript will help you. - -## 6. List subdomains - -Expand the query to also fetch the Domain's subdomains. `subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm) — pass `first: 20` to cap the page, and select `totalCount` to learn how many subdomains exist in total. - -```ts title="src/index.ts" ins={7-12,34-38} -const HelloWorldQuery = graphql(` - query HelloWorld($name: InterpretedName!) { - domain(by: { name: $name }) { - __typename - canonical { name { interpreted } } - owner { address } - subdomains(first: 20) { - totalCount - edges { - node { canonical { name { interpreted } } owner { address } } - } - } - } - } -`); - -async function main() { - const name = asInterpretedName("eth"); - - const result = await client.omnigraph.query({ - query: HelloWorldQuery, - variables: { name }, - }); - - if (result.errors) throw new Error(JSON.stringify(result.errors)); - if (!result.data?.domain) throw new Error(`Domain '${name}' not found`); - - const { domain } = result.data; - - console.log(`Name: ${domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : ""}`); - console.log(`Version: ${domain.__typename}`); - console.log(`Owner: ${domain.owner?.address ?? "0x0"}`); - - console.log(`\nSubdomains (showing 20 of ${domain.subdomains?.totalCount ?? 0}):`); - for (const { node } of domain.subdomains?.edges ?? []) { - const subName = node.canonical ? beautifyInterpretedName(node.canonical.name.interpreted) : ""; - console.log(` - ${subName} — Owner ${node.owner?.address ?? "0x0"}`); - } -} -``` - -Notice we're now writing the same canonical/owner rendering twice — once for the parent Domain and once inside the subdomain loop. We'll fix that next. - -To page beyond the first 20, the connection also exposes `pageInfo { hasNextPage endCursor }` and accepts an `after: String` cursor — see [Relay's connection spec](https://relay.dev/graphql/connections.htm). - -## 7. Extract a typed fragment - -Notice we're selecting the same fields (`canonical { name { interpreted } }`, `owner { address }`) on the parent Domain _and_ on each subdomain, and rendering them the same way. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed function that formats a Domain. - -```ts title="src/index.ts" ins={3,11-17,23,26,31,34-40,56,59} -import { asInterpretedName, beautifyInterpretedName } from "enssdk"; -import { createEnsNodeClient } from "enssdk/core"; -import { type FragmentOf, graphql, omnigraph, readFragment } from "enssdk/omnigraph"; - -// you may use a NameHash Hosted ENSNode instance -// learn more at https://ensnode.io/docs/hosted-instances -const ENSNODE_URL = process.env.ENSNODE_URL!; - -const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph); - -const DomainFragment = graphql(` - fragment DomainFragment on Domain { - __typename - canonical { name { interpreted } } - owner { address } - } -`); - -const HelloWorldQuery = graphql( - ` - query HelloWorld($name: InterpretedName!) { - domain(by: { name: $name }) { - ...DomainFragment - subdomains(first: 20) { - totalCount - edges { node { ...DomainFragment } } - } - } - } -`, - [DomainFragment], -); - -function formatDomain(data: FragmentOf): string { - // type-safe access to fragment data! - const domain = readFragment(DomainFragment, data); - const name = domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : ""; - const owner = domain.owner?.address ?? "0x0"; - return `${name} (${domain.__typename}) — Owner ${owner}`; -} - -async function main() { - const name = asInterpretedName("eth"); - - const result = await client.omnigraph.query({ - query: HelloWorldQuery, - variables: { name }, - }); - - if (result.errors) throw new Error(JSON.stringify(result.errors)); - if (!result.data?.domain) throw new Error(`Domain '${name}' not found`); - - const { domain } = result.data; - const totalCount = domain.subdomains?.totalCount ?? 0; - - console.log(formatDomain(domain)); - console.log(`\nSubdomains (showing 20 of ${totalCount}):`); - for (const { node } of domain.subdomains?.edges ?? []) { - console.log(` - ${formatDomain(node)}`); - } -} -``` - -`FragmentOf` is the opaque type for any selection that includes `...DomainFragment` — `formatDomain` accepts any of them, including each `node` in the subdomain edges. `readFragment(DomainFragment, data)` unwraps that opaque type to the typed fields you declared. - -## 8. Run it - -Point at a hosted ENSNode and go: - -```sh -ENSNODE_URL=https://api.alpha.ensnode.io npm start -``` - -You should see the `eth` Domain, followed by its first 20 subdomains and the total subdomain count. - -## Where to go next - -- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy GraphQL queries: account-owned domains, events, registrar permissions, full-text search, and more. -- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query. -- Building a React app? Use [`enskit`](/docs/integrate/integration-options/enskit) — same `graphql(...)` helper, with `useOmnigraphQuery` and a graphcache. - - - - - - +{ACTIVE_OMNIGRAPH_VERSION === "v1.14.1" ? : } diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx index a328732829..a16f7babe8 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx @@ -3,185 +3,15 @@ title: ENS Omnigraph GraphQL API description: Query the ENS Omnigraph API directly via HTTP from any language. --- -import { LinkCard } from '@astrojs/starlight/components'; -import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsNodeTip.astro'; - -The Omnigraph is **a GraphQL API** following the [Relay specification](https://relay.dev/graphql/connections.htm). There's no proprietary protocol or transport — any GraphQL client in any language works, from `curl` and `fetch` to [`urql`](https://nearform.com/open-source/urql/), [`Apollo`](https://www.apollographql.com/), [`graphql-request`](https://github.com/jasonkuhrt/graphql-request), and beyond. - -This guide walks you through the minimum: a single `fetch` call against `/api/omnigraph` — the same flow as our [omnigraph-graphql-example](https://github.com/namehash/ensnode/tree/main/examples/omnigraph-graphql-example). - -If you want end-to-end typed queries (via [`gql.tada`](https://gql-tada.0no.co/)) with editor autocomplete and a built-in client, use [`enssdk`](/docs/integrate/integration-options/enssdk) instead — but if you need to integrate from a language without first-class GraphQL tooling, or you're already in a stack with its own GraphQL client, this is the path. - - - -## 1. The endpoint - -The Omnigraph lives at: - -``` -POST {ENSNODE_URL}/api/omnigraph -Content-Type: application/json - -{ "query": "...", "variables": { ... } } -``` - -It returns `{ "data": ..., "errors": [...] }` per the standard GraphQL response shape. - -A minimum-viable hello world over `curl`: - -```sh -curl -sS -X POST \ - -H 'Content-Type: application/json' \ - -d '{"query":"{ domain(by: { name: \"eth\" }) { canonical { name { interpreted } } owner { address } } }"}' \ - https://api.alpha.ensnode.io/api/omnigraph -``` - -The rest of this guide builds the same thing in TypeScript using `fetch`, so you have something to extend. - -## 2. Scaffold a TypeScript project - -If you already have one, skip ahead to [Write the query](#3-write-the-query). - -```sh -mkdir my-ens-script && cd my-ens-script -npm init -y -mkdir src -``` - -Install [`tsx`](https://tsx.is/) so you can run TypeScript directly: - -```sh -npm install -D tsx typescript @types/node -``` - -Add a `start` script to `package.json`: - -```json title="package.json" ins={3-5} -{ - "type": "module", - "scripts": { - "start": "tsx src/index.ts" - } -} -``` - -## 3. Write the query - -Create `src/index.ts`. The whole script is a single `fetch` against `/api/omnigraph`. - -```ts title="src/index.ts" -// you may use a NameHash Hosted ENSNode instance -// learn more at https://ensnode.io/docs/hosted-instances -const ENSNODE_URL = process.env.ENSNODE_URL!; - -const HELLO_WORLD_QUERY = /* GraphQL */ ` - query HelloWorld($name: InterpretedName!) { - domain(by: { name: $name }) { - __typename - canonical { name { interpreted } } - owner { address } - subdomains(first: 20) { - totalCount - edges { node { __typename canonical { name { interpreted } } owner { address } } } - } - } - } -`; - -interface Domain { - __typename: "ENSv1Domain" | "ENSv2Domain"; - canonical: { name: { interpreted: string } } | null; - owner: { address: string } | null; -} - -interface QueryResult { - data?: { - domain: (Domain & { - subdomains: { - totalCount: number; - edges: { node: Domain }[]; - } | null; - }) | null; - } | null; - errors?: { message: string }[]; -} - -function formatDomain(domain: Domain): string { - const name = domain.canonical?.name.interpreted ?? ""; - const owner = domain.owner?.address ?? "0x0"; - return `${name} (${domain.__typename}) — Owner ${owner}`; -} - -async function main() { - const response = await fetch(new URL("/api/omnigraph", ENSNODE_URL), { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - query: HELLO_WORLD_QUERY, - variables: { name: "eth" }, - }), - }); - - if (!response.ok) { - throw new Error(`Request failed: ${response.status} ${response.statusText}`); - } - - const { data, errors } = (await response.json()) as QueryResult; - - if (errors) throw new Error(JSON.stringify(errors)); - if (!data?.domain) throw new Error("Domain 'eth' not found"); - - const { domain } = data; - const totalCount = domain.subdomains?.totalCount ?? 0; - - console.log(formatDomain(domain)); - console.log(`\nSubdomains (showing 20 of ${totalCount}):`); - for (const { node } of domain.subdomains?.edges ?? []) { - console.log(` - ${formatDomain(node)}`); - } -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); -``` - -A few things to notice: - -- **`InterpretedName` is a scalar.** From the wire's perspective it's just a string — the server validates the format. Pass `"eth"` as a plain string in `variables`. -- **`subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm).** Cursor through with `first`, `after`, `pageInfo { hasNextPage endCursor }` — same shape as any Relay-style API. -- **Hand-written types.** We're maintaining `interface Domain` and `interface QueryResult` ourselves here. If you want these generated and kept in sync with your queries automatically, use [`enssdk`](/docs/integrate/integration-options/enssdk). - -## 4. Run it - -```sh -ENSNODE_URL=https://api.alpha.ensnode.io npm start -``` - -You should see the `eth` Domain, its owner, and the first 20 of its subdomains. - -## Where to go next - -- Want typed queries with editor autocomplete and a real GraphQL client? Use [`enssdk`](/docs/integrate/integration-options/enssdk) — same API, with `gql.tada` types and an `EnsNodeClient`. -- Building a React app? Use [`enskit`](/docs/integrate/integration-options/enskit) — same `graphql(...)` helper plus `useOmnigraphQuery` and a graphcache. -- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more. -- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query. - - - - - - +import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active"; +import WalkthroughV1131 from "@components/walkthroughs/omnigraph-graphql-api/v1.13.1.mdx"; +import WalkthroughV1141 from "@components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx"; + +{/* + This walkthrough is version-locked to the production-deployed Omnigraph schema (see + `@data/omnigraph-examples/active`). Production lags `main`, and its schema differs, so we author one + partial per supported version under `@components/walkthroughs/omnigraph-graphql-api/` and render the + active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION; no edits here are needed. +*/} + +{ACTIVE_OMNIGRAPH_VERSION === "v1.14.1" ? : } From 145c974d3a2e66a83ad3e8b73918503fac52f947 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 22 May 2026 11:08:49 -0500 Subject: [PATCH 05/13] docs: version-lock the ENSv2 Quickstart to the active Omnigraph version Split integrate/index.mdx into per-version partials under @components/walkthroughs/quickstart/, rendering the partial matching ACTIVE_OMNIGRAPH_VERSION. The conceptual omnigraph/index page is left as-is (describes the canonical-object model; its 1.13.1 framing differs materially). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../walkthroughs/quickstart/v1.13.1.mdx | 207 +++++++++++++++++ .../walkthroughs/quickstart/v1.14.1.mdx | 207 +++++++++++++++++ .../src/content/docs/docs/integrate/index.mdx | 214 +----------------- 3 files changed, 423 insertions(+), 205 deletions(-) create mode 100644 docs/ensnode.io/src/components/walkthroughs/quickstart/v1.13.1.mdx create mode 100644 docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx diff --git a/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.13.1.mdx b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.13.1.mdx new file mode 100644 index 0000000000..46278125bb --- /dev/null +++ b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.13.1.mdx @@ -0,0 +1,207 @@ +import { LinkCard, CardGrid, Aside } from "@astrojs/starlight/components"; +import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanceSdkVersionWarning.astro"; +import OmnigraphAPIExample from "@components/organisms/OmnigraphAPIExample.astro"; + +## What is ENSv2? + +[ENSv2](https://ens.domains/ensv2) is the next generation of the [Ethereum Name Service](https://ens.domains) — a protocol upgrade that fundamentally changes how the ENS protocol works. + +:::tip[Prepare for ENSv2] +The ENSv2 upgrade to the ENS protocol is coming **Summer 2026**! Your app, regardless of how it interacts with names, needs to be updated to avoid being left behind. + + + Learn more about ENSv2 Readiness + +::: + +## What is the ENS Omnigraph? + +ENSNode fully supports ENSv2 via the [ENS Omnigraph API](/docs/integrate/omnigraph), the world's first and only _unified_ API over the full state of **both ENSv1 and ENSv2**. When ENSv2 launches in **Summer 2026**, ENSv1 continues to exist, and apps _must_ be updated to use the new protocol version. ENSNode takes the guesswork out of building on ENS, whether you need to resolve up-to-date records, search all Domains, or see which Domains a user owns (and much, much more). + +![ENS Omnigraph diagram](/ens-omnigraph-diagram.png) + +ENS Omnigraph supports both ENSv1 and ENSv2 **concurrently** within the **same unified data model**. This means you can integrate today (before ENSv2 launches) and continue with full ENSv2 support when it goes live, with zero downtime! + +## ENSNode's Integration Options + +ENSNode supports a full range of different integration options across the stack, whether you're using React, any JavaScript runtime, raw GraphQL, or looking to go deep and build a fully custom service using indexed ENS data. + + + +Here's a summary of some popular integration strategies: + +### 1. `enskit` + Omnigraph + +With `enskit`, leverage ENSNode and the Omnigraph to power your React components using `useOmnigraphQuery`. `enskit` comes with built-in type-safety, Omnigraph-specific cache directives, easy infinite pagination, and much much more. + + + + +```tsx +// this is fully typechecked and supports editor autocomplete! +const DomainFragment = graphql(` + fragment DomainFragment on Domain { + __typename + id + name + owner { id address } + } +`); + +// this is fully typechecked and supports editor autocomplete! +const DomainByNameQuery = graphql(` + query DomainByNameQuery($name: InterpretedName!) { + domain(by: { name: $name }) { + ...DomainFragment + subdomains { + edges { node { ...DomainFragment } } + } + } + } +`, + [DomainFragment], +); + +function RenderDomainFragment({ data }: { data: FragmentOf }) { + // type-safe access to fragment data! + const domain = readFragment(DomainFragment, data); + + return ( + <> + Name: {domain.name ? beautifyInterpretedName(domain.name) : 'Unnamed Domain'} + Protocol Version: {domain.__typename === 'ENSv1Domain' ? 'ENSv1' : 'ENSv2'} + Owner: {domain.owner ? domain.owner.address : 'Unowned'} + + ); +} + +export function RenderDomainAndSubdomains({ name }: { name: InterpretedName }) { + // `result` is fully typed! + const [result] = useOmnigraphQuery({ query: DomainByNameQuery, variables: { name } }); + const { data, fetching, error } = result; + + // some loading/error handling + if (!data && fetching) return

        Loading...

        ; + if (error) return

        Error: {error.message}

        ; + if (!data?.domain) return

        No domain was found with name '{name}'.

        ; + + // now we have type-safe access to Domain! + const domain = readFragment(DomainFragment, data.domain); + const { subdomains } = data.domain; + + return ( +
        + + +

        Subdomains:

        +
          + {subdomains?.edges.map((edge) => { + const { id } = readFragment(DomainFragment, edge.node); + return ( +
        • + +
        • + ); + })} +
        +
        + ); +} +``` + + + + + + + +### 2. `enssdk` + Omnigraph + +With `enssdk`, leverage ENSNode and the Omnigraph from any JavaScript runtime to power your frontend or backend apps. `enssdk` comes with built-in type-safety and editor autocomplete for Omnigraph queries. + + + + +```ts +// create and extend an EnsNodeClient with Omnigraph API support +const client = createEnsNodeClient({ url: process.env.ENSNODE_URL! }).extend(omnigraph); + +// this is fully typechecked and supports editor autocomplete! +const HelloWorldQuery = graphql(` + query HelloWorld { + domain(by: { name: "eth" }) { + id + name + owner { address } + } + } +`); + +// `result` is fully typed! +const result = await client.omnigraph.query({ query: HelloWorldQuery }); +``` + + + + + + + + +### 3. ENS Omnigraph GraphQL API + +The ENS Omnigraph API is a GraphQL API following the Relay specification, so you get built-in support for efficient infinite pagination and idiomatic access to all of the ENS protocol within a _unified_ ENSv1 + ENSv2 datamodel. + + + + + + + +### 4. Further Integration Options + +Beyond `enskit`, `enssdk`, and the Omnigraph GraphQL API, ENSNode exposes a deeper set of integration surfaces for advanced use cases: + +- **ENSDb** — query the indexed ENS dataset directly over Postgres for custom analytics or your own service layer. +- **enscli** — (coming soon) script ENSNode operations from the command line. +- **ensskills** — (coming soon) AI agent tooling for working with ENS data. +- **ensdb-cli** — (coming soon) share and load point-in-time ENSDb snapshots. +- **ENSEngine** (coming soon) — subscribe to ENS-aware webhooks driven by changes in ENSDb. + + diff --git a/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx new file mode 100644 index 0000000000..3730170921 --- /dev/null +++ b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx @@ -0,0 +1,207 @@ +import { LinkCard, CardGrid, Aside } from "@astrojs/starlight/components"; +import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanceSdkVersionWarning.astro"; +import OmnigraphAPIExample from "@components/organisms/OmnigraphAPIExample.astro"; + +## What is ENSv2? + +[ENSv2](https://ens.domains/ensv2) is the next generation of the [Ethereum Name Service](https://ens.domains) — a protocol upgrade that fundamentally changes how the ENS protocol works. + +:::tip[Prepare for ENSv2] +The ENSv2 upgrade to the ENS protocol is coming **Summer 2026**! Your app, regardless of how it interacts with names, needs to be updated to avoid being left behind. + + + Learn more about ENSv2 Readiness + +::: + +## What is the ENS Omnigraph? + +ENSNode fully supports ENSv2 via the [ENS Omnigraph API](/docs/integrate/omnigraph), the world's first and only _unified_ API over the full state of **both ENSv1 and ENSv2**. When ENSv2 launches in **Summer 2026**, ENSv1 continues to exist, and apps _must_ be updated to use the new protocol version. ENSNode takes the guesswork out of building on ENS, whether you need to resolve up-to-date records, search all Domains, or see which Domains a user owns (and much, much more). + +![ENS Omnigraph diagram](/ens-omnigraph-diagram.png) + +ENS Omnigraph supports both ENSv1 and ENSv2 **concurrently** within the **same unified data model**. This means you can integrate today (before ENSv2 launches) and continue with full ENSv2 support when it goes live, with zero downtime! + +## ENSNode's Integration Options + +ENSNode supports a full range of different integration options across the stack, whether you're using React, any JavaScript runtime, raw GraphQL, or looking to go deep and build a fully custom service using indexed ENS data. + + + +Here's a summary of some popular integration strategies: + +### 1. `enskit` + Omnigraph + +With `enskit`, leverage ENSNode and the Omnigraph to power your React components using `useOmnigraphQuery`. `enskit` comes with built-in type-safety, Omnigraph-specific cache directives, easy infinite pagination, and much much more. + + + + +```tsx +// this is fully typechecked and supports editor autocomplete! +const DomainFragment = graphql(` + fragment DomainFragment on Domain { + __typename + id + canonical { name { interpreted } } + owner { id address } + } +`); + +// this is fully typechecked and supports editor autocomplete! +const DomainByNameQuery = graphql(` + query DomainByNameQuery($name: InterpretedName!) { + domain(by: { name: $name }) { + ...DomainFragment + subdomains { + edges { node { ...DomainFragment } } + } + } + } +`, + [DomainFragment], +); + +function RenderDomainFragment({ data }: { data: FragmentOf }) { + // type-safe access to fragment data! + const domain = readFragment(DomainFragment, data); + + return ( + <> + Name: {domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : 'Unnamed Domain'} + Protocol Version: {domain.__typename === 'ENSv1Domain' ? 'ENSv1' : 'ENSv2'} + Owner: {domain.owner ? domain.owner.address : 'Unowned'} + + ); +} + +export function RenderDomainAndSubdomains({ name }: { name: InterpretedName }) { + // `result` is fully typed! + const [result] = useOmnigraphQuery({ query: DomainByNameQuery, variables: { name } }); + const { data, fetching, error } = result; + + // some loading/error handling + if (!data && fetching) return

        Loading...

        ; + if (error) return

        Error: {error.message}

        ; + if (!data?.domain) return

        No domain was found with name '{name}'.

        ; + + // now we have type-safe access to Domain! + const domain = readFragment(DomainFragment, data.domain); + const { subdomains } = data.domain; + + return ( +
        + + +

        Subdomains:

        +
          + {subdomains?.edges.map((edge) => { + const { id } = readFragment(DomainFragment, edge.node); + return ( +
        • + +
        • + ); + })} +
        +
        + ); +} +``` + + + + + + + +### 2. `enssdk` + Omnigraph + +With `enssdk`, leverage ENSNode and the Omnigraph from any JavaScript runtime to power your frontend or backend apps. `enssdk` comes with built-in type-safety and editor autocomplete for Omnigraph queries. + + + + +```ts +// create and extend an EnsNodeClient with Omnigraph API support +const client = createEnsNodeClient({ url: process.env.ENSNODE_URL! }).extend(omnigraph); + +// this is fully typechecked and supports editor autocomplete! +const HelloWorldQuery = graphql(` + query HelloWorld { + domain(by: { name: "eth" }) { + id + canonical { name { interpreted } } + owner { address } + } + } +`); + +// `result` is fully typed! +const result = await client.omnigraph.query({ query: HelloWorldQuery }); +``` + + + + + + + + +### 3. ENS Omnigraph GraphQL API + +The ENS Omnigraph API is a GraphQL API following the Relay specification, so you get built-in support for efficient infinite pagination and idiomatic access to all of the ENS protocol within a _unified_ ENSv1 + ENSv2 datamodel. + + + + + + + +### 4. Further Integration Options + +Beyond `enskit`, `enssdk`, and the Omnigraph GraphQL API, ENSNode exposes a deeper set of integration surfaces for advanced use cases: + +- **ENSDb** — query the indexed ENS dataset directly over Postgres for custom analytics or your own service layer. +- **enscli** — (coming soon) script ENSNode operations from the command line. +- **ensskills** — (coming soon) AI agent tooling for working with ENS data. +- **ensdb-cli** — (coming soon) share and load point-in-time ENSDb snapshots. +- **ENSEngine** (coming soon) — subscribe to ENS-aware webhooks driven by changes in ENSDb. + + diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx index fe587e22d0..b4c337ad05 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx @@ -6,210 +6,14 @@ sidebar: order: 1 --- -import { LinkCard, CardGrid, Aside } from "@astrojs/starlight/components"; -import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanceSdkVersionWarning.astro"; -import OmnigraphAPIExample from "@components/organisms/OmnigraphAPIExample.astro"; +import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active"; +import QuickstartV1131 from "@components/walkthroughs/quickstart/v1.13.1.mdx"; +import QuickstartV1141 from "@components/walkthroughs/quickstart/v1.14.1.mdx"; -## What is ENSv2? +{/* + Version-locked to the production-deployed Omnigraph schema (see `@data/omnigraph-examples/active`). + Author one partial per supported version under `@components/walkthroughs/quickstart/` and render the + active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION; no edits here are needed. +*/} -[ENSv2](https://ens.domains/ensv2) is the next generation of the [Ethereum Name Service](https://ens.domains) — a protocol upgrade that fundamentally changes how the ENS protocol works. - -:::tip[Prepare for ENSv2] -The ENSv2 upgrade to the ENS protocol is coming **Summer 2026**! Your app, regardless of how it interacts with names, needs to be updated to avoid being left behind. - - - Learn more about ENSv2 Readiness - -::: - -## What is the ENS Omnigraph? - -ENSNode fully supports ENSv2 via the [ENS Omnigraph API](/docs/integrate/omnigraph), the world's first and only _unified_ API over the full state of **both ENSv1 and ENSv2**. When ENSv2 launches in **Summer 2026**, ENSv1 continues to exist, and apps _must_ be updated to use the new protocol version. ENSNode takes the guesswork out of building on ENS, whether you need to resolve up-to-date records, search all Domains, or see which Domains a user owns (and much, much more). - -![ENS Omnigraph diagram](/ens-omnigraph-diagram.png) - -ENS Omnigraph supports both ENSv1 and ENSv2 **concurrently** within the **same unified data model**. This means you can integrate today (before ENSv2 launches) and continue with full ENSv2 support when it goes live, with zero downtime! - -## ENSNode's Integration Options - -ENSNode supports a full range of different integration options across the stack, whether you're using React, any JavaScript runtime, raw GraphQL, or looking to go deep and build a fully custom service using indexed ENS data. - - - -Here's a summary of some popular integration strategies: - -### 1. `enskit` + Omnigraph - -With `enskit`, leverage ENSNode and the Omnigraph to power your React components using `useOmnigraphQuery`. `enskit` comes with built-in type-safety, Omnigraph-specific cache directives, easy infinite pagination, and much much more. - - - - -```tsx -// this is fully typechecked and supports editor autocomplete! -const DomainFragment = graphql(` - fragment DomainFragment on Domain { - __typename - id - canonical { name { interpreted } } - owner { id address } - } -`); - -// this is fully typechecked and supports editor autocomplete! -const DomainByNameQuery = graphql(` - query DomainByNameQuery($name: InterpretedName!) { - domain(by: { name: $name }) { - ...DomainFragment - subdomains { - edges { node { ...DomainFragment } } - } - } - } -`, - [DomainFragment], -); - -function RenderDomainFragment({ data }: { data: FragmentOf }) { - // type-safe access to fragment data! - const domain = readFragment(DomainFragment, data); - - return ( - <> - Name: {domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : 'Unnamed Domain'} - Protocol Version: {domain.__typename === 'ENSv1Domain' ? 'ENSv1' : 'ENSv2'} - Owner: {domain.owner ? domain.owner.address : 'Unowned'} - - ); -} - -export function RenderDomainAndSubdomains({ name }: { name: InterpretedName }) { - // `result` is fully typed! - const [result] = useOmnigraphQuery({ query: DomainByNameQuery, variables: { name } }); - const { data, fetching, error } = result; - - // some loading/error handling - if (!data && fetching) return

        Loading...

        ; - if (error) return

        Error: {error.message}

        ; - if (!data?.domain) return

        No domain was found with name '{name}'.

        ; - - // now we have type-safe access to Domain! - const domain = readFragment(DomainFragment, data.domain); - const { subdomains } = data.domain; - - return ( -
        - - -

        Subdomains:

        -
          - {subdomains?.edges.map((edge) => { - const { id } = readFragment(DomainFragment, edge.node); - return ( -
        • - -
        • - ); - })} -
        -
        - ); -} -``` - - - - - - - -### 2. `enssdk` + Omnigraph - -With `enssdk`, leverage ENSNode and the Omnigraph from any JavaScript runtime to power your frontend or backend apps. `enssdk` comes with built-in type-safety and editor autocomplete for Omnigraph queries. - - - - -```ts -// create and extend an EnsNodeClient with Omnigraph API support -const client = createEnsNodeClient({ url: process.env.ENSNODE_URL! }).extend(omnigraph); - -// this is fully typechecked and supports editor autocomplete! -const HelloWorldQuery = graphql(` - query HelloWorld { - domain(by: { name: "eth" }) { - id - canonical { name { interpreted } } - owner { address } - } - } -`); - -// `result` is fully typed! -const result = await client.omnigraph.query({ query: HelloWorldQuery }); -``` - - - - - - - - -### 3. ENS Omnigraph GraphQL API - -The ENS Omnigraph API is a GraphQL API following the Relay specification, so you get built-in support for efficient infinite pagination and idiomatic access to all of the ENS protocol within a _unified_ ENSv1 + ENSv2 datamodel. - - - - - - - -### 4. Further Integration Options - -Beyond `enskit`, `enssdk`, and the Omnigraph GraphQL API, ENSNode exposes a deeper set of integration surfaces for advanced use cases: - -- **ENSDb** — query the indexed ENS dataset directly over Postgres for custom analytics or your own service layer. -- **enscli** — (coming soon) script ENSNode operations from the command line. -- **ensskills** — (coming soon) AI agent tooling for working with ENS data. -- **ensdb-cli** — (coming soon) share and load point-in-time ENSDb snapshots. -- **ENSEngine** (coming soon) — subscribe to ENS-aware webhooks driven by changes in ENSDb. - - +{ACTIVE_OMNIGRAPH_VERSION === "v1.14.1" ? : } From a60e8f5dba9fa4f2775a14fd3185a3a6cbe64661 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 22 May 2026 11:28:16 -0500 Subject: [PATCH 06/13] docs/examples: address review findings - v1.14.1 walkthrough partials now use `canonical { name { beautified } }` rendered directly (dropping `beautifyInterpretedName`/`interpreted`), matching the migrated example apps they transcribe. - Version selectors in the route pages switch on ACTIVE_OMNIGRAPH_VERSION and throw on an unmapped version, matching the schema explorer's fail-fast behavior instead of silently rendering the oldest partial. - enskit example: parent back-link now links by DomainId whenever a parent exists (falling back to the id as label when the parent has no Canonical Name). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../walkthroughs/enskit/v1.14.1.mdx | 22 ++++++++--------- .../walkthroughs/enssdk/v1.14.1.mdx | 24 +++++++++---------- .../omnigraph-graphql-api/v1.14.1.mdx | 10 ++++---- .../walkthroughs/quickstart/v1.14.1.mdx | 6 ++--- .../src/content/docs/docs/integrate/index.mdx | 14 +++++++++-- .../integration-options/enskit/index.mdx | 14 +++++++++-- .../integration-options/enssdk/index.mdx | 14 +++++++++-- .../omnigraph-graphql-api.mdx | 14 +++++++++-- .../enskit-react-example/src/DomainView.tsx | 9 +++++-- 9 files changed, 86 insertions(+), 41 deletions(-) diff --git a/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx index 09af7d51c6..96f90143f3 100644 --- a/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx +++ b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx @@ -104,13 +104,13 @@ An **InterpretedName** is a Name whose labels are each either normalized or repr ```tsx title="src/DomainView.tsx" import { graphql, useOmnigraphQuery } from "enskit/react/omnigraph"; -import { asInterpretedName, beautifyInterpretedName } from "enssdk"; +import { asInterpretedName } from "enssdk"; const DomainByNameQuery = graphql(` query DomainByName($name: InterpretedName!) { domain(by: { name: $name }) { __typename - canonical { name { interpreted } } + canonical { name { beautified } } owner { address } } } @@ -136,7 +136,7 @@ export function DomainView() {

        {domain.canonical - ? beautifyInterpretedName(domain.canonical.name.interpreted) + ? domain.canonical.name.beautified : "Unnamed Domain"}

        Version: {domain.__typename}

        @@ -163,12 +163,12 @@ const DomainByNameQuery = graphql(` query DomainByName($name: InterpretedName!) { domain(by: { name: $name }) { __typename - canonical { name { interpreted } } + canonical { name { beautified } } owner { address } subdomains { edges { node { - canonical { name { interpreted } } + canonical { name { beautified } } owner { address } } } @@ -195,7 +195,7 @@ export function DomainView() { return (
        -

        {domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : "Unnamed Domain"}

        +

        {domain.canonical ? domain.canonical.name.beautified : "Unnamed Domain"}

        Version: {domain.__typename}

        Owner: {domain.owner?.address ?? "0x0"}

        @@ -204,7 +204,7 @@ export function DomainView() { {domain.subdomains?.edges.map(({ node }, i) => (
      • {node.canonical - ? beautifyInterpretedName(node.canonical.name.interpreted) + ? node.canonical.name.beautified : unnamed}{" "} — Owner {node.owner?.address ?? "0x0"}
      • @@ -217,7 +217,7 @@ export function DomainView() { ## 7. Extract a typed fragment -Notice we're selecting the same fields (`canonical { name { interpreted } }`, `owner { address }`) on the parent Domain _and_ on each subdomain. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed shape for components that render a Domain. +Notice we're selecting the same fields (`canonical { name { beautified } }`, `owner { address }`) on the parent Domain _and_ on each subdomain. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed shape for components that render a Domain. ```tsx title="src/DomainView.tsx" ins={1,2,5,9-15,21,23,28,31-48,66,72} import { @@ -226,12 +226,12 @@ import { readFragment, useOmnigraphQuery, } from "enskit/react/omnigraph"; -import { asInterpretedName, beautifyInterpretedName } from "enssdk"; +import { asInterpretedName } from "enssdk"; const DomainFragment = graphql(` fragment DomainFragment on Domain { __typename - canonical { name { interpreted } } + canonical { name { beautified } } owner { address } } `); @@ -258,7 +258,7 @@ function RenderDomain({ data }: { data: FragmentOf }) { <> {domain.canonical - ? beautifyInterpretedName(domain.canonical.name.interpreted) + ? domain.canonical.name.beautified : "Unnamed Domain"} {" "} ({domain.__typename}){" "} diff --git a/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx index 100a42c207..85d881725c 100644 --- a/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx +++ b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx @@ -111,7 +111,7 @@ An **InterpretedName** is a Name whose labels are each either normalized or repr ```ts title="src/index.ts" // existing imports... -import { asInterpretedName, beautifyInterpretedName } from "enssdk"; +import { asInterpretedName } from "enssdk"; import { graphql, omnigraph } from "enssdk/omnigraph"; // existing client... @@ -121,7 +121,7 @@ const HelloWorldQuery = graphql(` query HelloWorld($name: InterpretedName!) { domain(by: { name: $name }) { __typename - canonical { name { interpreted } } + canonical { name { beautified } } owner { address } } } @@ -140,7 +140,7 @@ async function main() { const { domain } = result.data; - console.log(`Name: ${domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : ""}`); + console.log(`Name: ${domain.canonical ? domain.canonical.name.beautified : ""}`); console.log(`Version: ${domain.__typename}`); console.log(`Owner: ${domain.owner?.address ?? "0x0"}`); } @@ -155,7 +155,7 @@ A few things to notice: - `graphql(...)` parses your query at typecheck time. Hover over `result.data` and you'll see it's typed exactly to your selection set — try removing `owner { address }` from the query and watch the access below become a type error. - `domain` is a union of `ENSv1Domain | ENSv2Domain` (both implement the `Domain` interface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query — `__typename` tells you which one you got. -- `canonical` is `null` for non-canonical Domains (e.g. Domains whose name cannot be inferred). When non-null, `canonical.name.interpreted` is the Domain's Canonical Name as an InterpretedName. **Always** guard the access; TypeScript will help you. +- `canonical` is `null` for non-canonical Domains (e.g. Domains whose name cannot be inferred). When non-null, `canonical.name.beautified` is the Domain's Canonical Name, beautified for display. **Always** guard the access; TypeScript will help you. ## 6. List subdomains @@ -166,12 +166,12 @@ const HelloWorldQuery = graphql(` query HelloWorld($name: InterpretedName!) { domain(by: { name: $name }) { __typename - canonical { name { interpreted } } + canonical { name { beautified } } owner { address } subdomains(first: 20) { totalCount edges { - node { canonical { name { interpreted } } owner { address } } + node { canonical { name { beautified } } owner { address } } } } } @@ -191,13 +191,13 @@ async function main() { const { domain } = result.data; - console.log(`Name: ${domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : ""}`); + console.log(`Name: ${domain.canonical ? domain.canonical.name.beautified : ""}`); console.log(`Version: ${domain.__typename}`); console.log(`Owner: ${domain.owner?.address ?? "0x0"}`); console.log(`\nSubdomains (showing 20 of ${domain.subdomains?.totalCount ?? 0}):`); for (const { node } of domain.subdomains?.edges ?? []) { - const subName = node.canonical ? beautifyInterpretedName(node.canonical.name.interpreted) : ""; + const subName = node.canonical ? node.canonical.name.beautified : ""; console.log(` - ${subName} — Owner ${node.owner?.address ?? "0x0"}`); } } @@ -209,10 +209,10 @@ To page beyond the first 20, the connection also exposes `pageInfo { hasNextPage ## 7. Extract a typed fragment -Notice we're selecting the same fields (`canonical { name { interpreted } }`, `owner { address }`) on the parent Domain _and_ on each subdomain, and rendering them the same way. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed function that formats a Domain. +Notice we're selecting the same fields (`canonical { name { beautified } }`, `owner { address }`) on the parent Domain _and_ on each subdomain, and rendering them the same way. Extract a `DomainFragment` to deduplicate the selection — and get a reusable, fully-typed function that formats a Domain. ```ts title="src/index.ts" ins={3,11-17,23,26,31,34-40,56,59} -import { asInterpretedName, beautifyInterpretedName } from "enssdk"; +import { asInterpretedName } from "enssdk"; import { createEnsNodeClient } from "enssdk/core"; import { type FragmentOf, graphql, omnigraph, readFragment } from "enssdk/omnigraph"; @@ -225,7 +225,7 @@ const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph); const DomainFragment = graphql(` fragment DomainFragment on Domain { __typename - canonical { name { interpreted } } + canonical { name { beautified } } owner { address } } `); @@ -248,7 +248,7 @@ const HelloWorldQuery = graphql( function formatDomain(data: FragmentOf): string { // type-safe access to fragment data! const domain = readFragment(DomainFragment, data); - const name = domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : ""; + const name = domain.canonical ? domain.canonical.name.beautified : ""; const owner = domain.owner?.address ?? "0x0"; return `${name} (${domain.__typename}) — Owner ${owner}`; } diff --git a/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx index 7b46a1d861..ee7a27c425 100644 --- a/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx +++ b/docs/ensnode.io/src/components/walkthroughs/omnigraph-graphql-api/v1.14.1.mdx @@ -27,7 +27,7 @@ A minimum-viable hello world over `curl`: ```sh curl -sS -X POST \ -H 'Content-Type: application/json' \ - -d '{"query":"{ domain(by: { name: \"eth\" }) { canonical { name { interpreted } } owner { address } } }"}' \ + -d '{"query":"{ domain(by: { name: \"eth\" }) { canonical { name { beautified } } owner { address } } }"}' \ https://api.v2-sepolia.blue.ensnode.io/api/omnigraph ``` @@ -73,11 +73,11 @@ const HELLO_WORLD_QUERY = /* GraphQL */ ` query HelloWorld($name: InterpretedName!) { domain(by: { name: $name }) { __typename - canonical { name { interpreted } } + canonical { name { beautified } } owner { address } subdomains(first: 20) { totalCount - edges { node { __typename canonical { name { interpreted } } owner { address } } } + edges { node { __typename canonical { name { beautified } } owner { address } } } } } } @@ -85,7 +85,7 @@ const HELLO_WORLD_QUERY = /* GraphQL */ ` interface Domain { __typename: "ENSv1Domain" | "ENSv2Domain"; - canonical: { name: { interpreted: string } } | null; + canonical: { name: { beautified: string } } | null; owner: { address: string } | null; } @@ -102,7 +102,7 @@ interface QueryResult { } function formatDomain(domain: Domain): string { - const name = domain.canonical?.name.interpreted ?? ""; + const name = domain.canonical?.name.beautified ?? ""; const owner = domain.owner?.address ?? "0x0"; return `${name} (${domain.__typename}) — Owner ${owner}`; } diff --git a/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx index 3730170921..e546b000b4 100644 --- a/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx +++ b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx @@ -46,7 +46,7 @@ const DomainFragment = graphql(` fragment DomainFragment on Domain { __typename id - canonical { name { interpreted } } + canonical { name { beautified } } owner { id address } } `); @@ -71,7 +71,7 @@ function RenderDomainFragment({ data }: { data: FragmentOf - Name: {domain.canonical ? beautifyInterpretedName(domain.canonical.name.interpreted) : 'Unnamed Domain'} + Name: {domain.canonical ? domain.canonical.name.beautified : 'Unnamed Domain'} Protocol Version: {domain.__typename === 'ENSv1Domain' ? 'ENSv1' : 'ENSv2'} Owner: {domain.owner ? domain.owner.address : 'Unowned'} @@ -145,7 +145,7 @@ const HelloWorldQuery = graphql(` query HelloWorld { domain(by: { name: "eth" }) { id - canonical { name { interpreted } } + canonical { name { beautified } } owner { address } } } diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx index b4c337ad05..e74f1efcfe 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/index.mdx @@ -13,7 +13,17 @@ import QuickstartV1141 from "@components/walkthroughs/quickstart/v1.14.1.mdx"; {/* Version-locked to the production-deployed Omnigraph schema (see `@data/omnigraph-examples/active`). Author one partial per supported version under `@components/walkthroughs/quickstart/` and render the - active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION; no edits here are needed. + active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION (and, for a new version, add its partial and + a `case` below — an unmapped version throws at build rather than silently rendering stale docs). */} -{ACTIVE_OMNIGRAPH_VERSION === "v1.14.1" ? : } +{(() => { + switch (ACTIVE_OMNIGRAPH_VERSION) { + case "v1.13.1": + return ; + case "v1.14.1": + return ; + default: + throw new Error(`No quickstart partial for Omnigraph version "${ACTIVE_OMNIGRAPH_VERSION}".`); + } +})()} diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx index c4233ff242..0bc69f4c87 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/index.mdx @@ -10,7 +10,17 @@ import WalkthroughV1141 from "@components/walkthroughs/enskit/v1.14.1.mdx"; {/* Version-locked to the production-deployed Omnigraph schema (see `@data/omnigraph-examples/active`). Author one partial per supported version under `@components/walkthroughs/enskit/` and render the - active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION; no edits here are needed. + active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION (and, for a new version, add its partial and + a `case` below — an unmapped version throws at build rather than silently rendering stale docs). */} -{ACTIVE_OMNIGRAPH_VERSION === "v1.14.1" ? : } +{(() => { + switch (ACTIVE_OMNIGRAPH_VERSION) { + case "v1.13.1": + return ; + case "v1.14.1": + return ; + default: + throw new Error(`No walkthrough partial for Omnigraph version "${ACTIVE_OMNIGRAPH_VERSION}".`); + } +})()} diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx index 9595e97ae8..2d09563c1e 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/index.mdx @@ -10,7 +10,17 @@ import WalkthroughV1141 from "@components/walkthroughs/enssdk/v1.14.1.mdx"; {/* Version-locked to the production-deployed Omnigraph schema (see `@data/omnigraph-examples/active`). Author one partial per supported version under `@components/walkthroughs/enssdk/` and render the - active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION; no edits here are needed. + active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION (and, for a new version, add its partial and + a `case` below — an unmapped version throws at build rather than silently rendering stale docs). */} -{ACTIVE_OMNIGRAPH_VERSION === "v1.14.1" ? : } +{(() => { + switch (ACTIVE_OMNIGRAPH_VERSION) { + case "v1.13.1": + return ; + case "v1.14.1": + return ; + default: + throw new Error(`No walkthrough partial for Omnigraph version "${ACTIVE_OMNIGRAPH_VERSION}".`); + } +})()} diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx index a16f7babe8..169d38779a 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/omnigraph-graphql-api.mdx @@ -11,7 +11,17 @@ import WalkthroughV1141 from "@components/walkthroughs/omnigraph-graphql-api/v1. This walkthrough is version-locked to the production-deployed Omnigraph schema (see `@data/omnigraph-examples/active`). Production lags `main`, and its schema differs, so we author one partial per supported version under `@components/walkthroughs/omnigraph-graphql-api/` and render the - active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION; no edits here are needed. + active one. Promote by bumping ACTIVE_OMNIGRAPH_VERSION (and, for a new version, add its partial and + a `case` below — an unmapped version throws at build rather than silently rendering stale docs). */} -{ACTIVE_OMNIGRAPH_VERSION === "v1.14.1" ? : } +{(() => { + switch (ACTIVE_OMNIGRAPH_VERSION) { + case "v1.13.1": + return ; + case "v1.14.1": + return ; + default: + throw new Error(`No walkthrough partial for Omnigraph version "${ACTIVE_OMNIGRAPH_VERSION}".`); + } +})()} diff --git a/examples/enskit-react-example/src/DomainView.tsx b/examples/enskit-react-example/src/DomainView.tsx index 9bfdc04b81..d759d43c94 100644 --- a/examples/enskit-react-example/src/DomainView.tsx +++ b/examples/enskit-react-example/src/DomainView.tsx @@ -134,9 +134,14 @@ function RenderDomain({ by }: { by: DomainBy }) {

        Version: {domain.__typename}

        - {data.domain.parent?.canonical && ( + {data.domain.parent && ( + // always link the parent by its (stable) DomainId; fall back to the id as the label when the + // parent has no Canonical Name - ← {data.domain.parent.canonical.name.beautified} + ←{" "} + {data.domain.parent.canonical + ? data.domain.parent.canonical.name.beautified + : data.domain.parent.id} )} From 82566296f9c88787f3b412ec88e18f6e3d0728fc Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 22 May 2026 11:57:17 -0500 Subject: [PATCH 07/13] docs: simplify HostedInstanceSdkVersionWarning to always show both SDKs Drop the `for` prop branching; the warning now always mentions enssdk + enskit and shows both install commands. Callers still passing `for=...` are harmless (the attribute is ignored). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../HostedInstanceSdkVersionWarning.astro | 35 +++---------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro b/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro index c40e9eaeda..0d8cac5a28 100644 --- a/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro +++ b/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro @@ -3,43 +3,16 @@ import { Aside } from "@astrojs/starlight/components"; import { ACTIVE_OMNIGRAPH_VERSION } from "@data/omnigraph-examples/active"; -interface Props { - /** - * Which SDK(s) the warning is for. - * - "enssdk": only enssdk - * - "enskit": both enskit and enssdk (enskit depends on enssdk) - * - "both": mention both enssdk and enskit, show both install commands - */ - for?: "enssdk" | "enskit" | "both"; -} - -const { for: target = "enssdk" } = Astro.props; - -const isEnskit = target === "enskit"; -const isBoth = target === "both"; - // The SDK version is locked to the production-deployed Omnigraph version (see // `@data/omnigraph-examples/active`). The SDK bundles the Omnigraph schema, so pinning the matching // version keeps gql.tada's generated types aligned with the deployed API. Updates on promotion. -const sdkVersion = ACTIVE_OMNIGRAPH_VERSION.replace(/^v/, ""); - -const sdkList = isEnskit - ? `enskit@${sdkVersion} and enssdk@${sdkVersion}` - : isBoth - ? `enssdk@${sdkVersion} (and enskit@${sdkVersion} when using React)` - : `enssdk@${sdkVersion}`; +const VERSION = ACTIVE_OMNIGRAPH_VERSION.replace(/^v/, ""); --- From 7672715b5ec310d0057b25c355a41dac05a3c79c Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 22 May 2026 12:02:39 -0500 Subject: [PATCH 08/13] docs: drop dead `for` plumbing now that the SDK warning always shows both HostedInstanceSdkVersionWarning no longer branches on `for`, so remove the prop passthrough from IntegrateHostedEnsNodeTip and the now-inert `for="..."` props at every call site. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../molecules/IntegrateHostedEnsNodeTip.astro | 14 +------------- .../src/components/walkthroughs/enskit/v1.13.1.mdx | 2 +- .../src/components/walkthroughs/enskit/v1.14.1.mdx | 2 +- .../src/components/walkthroughs/enssdk/v1.13.1.mdx | 2 +- .../src/components/walkthroughs/enssdk/v1.14.1.mdx | 2 +- .../components/walkthroughs/quickstart/v1.13.1.mdx | 4 ++-- .../components/walkthroughs/quickstart/v1.14.1.mdx | 4 ++-- .../src/content/docs/docs/hosted-instances.mdx | 2 +- .../integration-options/enskit/example.mdx | 2 +- .../integration-options/enssdk/example.mdx | 2 +- 10 files changed, 12 insertions(+), 24 deletions(-) diff --git a/docs/ensnode.io/src/components/molecules/IntegrateHostedEnsNodeTip.astro b/docs/ensnode.io/src/components/molecules/IntegrateHostedEnsNodeTip.astro index eaa37598be..94f34e23c9 100644 --- a/docs/ensnode.io/src/components/molecules/IntegrateHostedEnsNodeTip.astro +++ b/docs/ensnode.io/src/components/molecules/IntegrateHostedEnsNodeTip.astro @@ -1,24 +1,12 @@ --- import { Aside, LinkCard } from "@astrojs/starlight/components"; import HostedInstanceSdkVersionWarning from "./HostedInstanceSdkVersionWarning.astro"; - -interface Props { - /** - * Which SDK the warning should target. Passed through to HostedInstanceSdkVersionWarning. - * - "enssdk": enssdk-only warning - * - "enskit": both enskit and enssdk warning - * - "both": mention both enssdk and enskit, show both install commands (default) - */ - for?: "enssdk" | "enskit" | "both"; -} - -const { for: target = "both" } = Astro.props; --- - + + ## 1. Scaffold a React app diff --git a/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx index 96f90143f3..ee1a77508e 100644 --- a/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx +++ b/docs/ensnode.io/src/components/walkthroughs/enskit/v1.14.1.mdx @@ -5,7 +5,7 @@ import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsN This guide walks you from an empty directory to a working React component that renders an [ENS Domain](/docs/concepts/the-ens-protocol) and a paginated list of its subdomains — the same flow as the [`DomainView`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in our example app. - + ## 1. Scaffold a React app diff --git a/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.13.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.13.1.mdx index 8ba78bc644..a7e169569d 100644 --- a/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.13.1.mdx +++ b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.13.1.mdx @@ -7,7 +7,7 @@ import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsN This guide walks you from an empty directory to a working TypeScript script that queries the `eth` Domain and queries its subdomains — the same flow as our [enssdk-example](https://github.com/namehash/ensnode/tree/main/examples/enssdk-example). - + ## 1. Scaffold a TypeScript project diff --git a/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx index 85d881725c..eb410b9adc 100644 --- a/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx +++ b/docs/ensnode.io/src/components/walkthroughs/enssdk/v1.14.1.mdx @@ -7,7 +7,7 @@ import IntegrateHostedEnsNodeTip from '@components/molecules/IntegrateHostedEnsN This guide walks you from an empty directory to a working TypeScript script that queries the `eth` Domain and queries its subdomains — the same flow as our [enssdk-example](https://github.com/namehash/ensnode/tree/main/examples/enssdk-example). - + ## 1. Scaffold a TypeScript project diff --git a/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.13.1.mdx b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.13.1.mdx index 46278125bb..728ded685a 100644 --- a/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.13.1.mdx +++ b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.13.1.mdx @@ -37,7 +37,7 @@ Here's a summary of some popular integration strategies: With `enskit`, leverage ENSNode and the Omnigraph to power your React components using `useOmnigraphQuery`. `enskit` comes with built-in type-safety, Omnigraph-specific cache directives, easy infinite pagination, and much much more. - + ```tsx @@ -133,7 +133,7 @@ export function RenderDomainAndSubdomains({ name }: { name: InterpretedName }) { With `enssdk`, leverage ENSNode and the Omnigraph from any JavaScript runtime to power your frontend or backend apps. `enssdk` comes with built-in type-safety and editor autocomplete for Omnigraph queries. - + ```ts diff --git a/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx index e546b000b4..2f86d7dbe0 100644 --- a/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx +++ b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.14.1.mdx @@ -37,7 +37,7 @@ Here's a summary of some popular integration strategies: With `enskit`, leverage ENSNode and the Omnigraph to power your React components using `useOmnigraphQuery`. `enskit` comes with built-in type-safety, Omnigraph-specific cache directives, easy infinite pagination, and much much more. - + ```tsx @@ -133,7 +133,7 @@ export function RenderDomainAndSubdomains({ name }: { name: InterpretedName }) { With `enssdk`, leverage ENSNode and the Omnigraph from any JavaScript runtime to power your frontend or backend apps. `enssdk` comes with built-in type-safety and editor autocomplete for Omnigraph queries. - + ```ts diff --git a/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx b/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx index 99e6fbbf5a..946eee7352 100644 --- a/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx +++ b/docs/ensnode.io/src/content/docs/docs/hosted-instances.mdx @@ -15,7 +15,7 @@ NameHash Labs provides freely available hosted instances for ENS developers look These instances are provided free of charge with no API key required, have no rate limiting, and are maintained and monitored by the NameHash Labs team. - + ### Available instance configurations diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/example.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/example.mdx index 3ff146694a..9aef54179a 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/example.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enskit/example.mdx @@ -12,7 +12,7 @@ import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanc This playground loads the same source as [`enskit-react-example`](https://github.com/namehash/ensnode/tree/main/examples/enskit-react-example): a Vite + React app with routing, domain and account browsers, registry cache, and search — powered by [`enskit`](/docs/integrate/integration-options/enskit) and the [ENS Omnigraph API](/docs/integrate/omnigraph). - + :::note[First load may take a moment] The editor runs entirely in your browser. Downloading of **enskit**, **enssdk** and their heavy dependencies may take 30-60 seconds. diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/example.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/example.mdx index 3f9940c0f3..4e1b87f13c 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/example.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/integration-options/enssdk/example.mdx @@ -12,7 +12,7 @@ import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanc This playground loads the same source as [`enssdk-example`](https://github.com/namehash/ensnode/tree/main/examples/enssdk-example): a TypeScript script that queries the `eth` domain and lists its first 20 subdomains via [`enssdk`](/docs/integrate/integration-options/enssdk) and the [ENS Omnigraph API](/docs/integrate/omnigraph). - + :::note[First load may take a few minutes] The embedded StackBlitz editor runs entirely in your browser. Downloading and installing all npm packages may take a few minutes. Watch the install progress in the terminal of the StackBlitz editor. From e22335ad1836222b13aafabce4b595b29fc158d7 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 22 May 2026 12:04:13 -0500 Subject: [PATCH 09/13] checkpoint --- .../molecules/HostedInstanceSdkVersionWarning.astro | 2 +- .../components/organisms/OmnigraphSchemaDocExplorer.tsx | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro b/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro index 0d8cac5a28..9aacdf3c6c 100644 --- a/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro +++ b/docs/ensnode.io/src/components/molecules/HostedInstanceSdkVersionWarning.astro @@ -10,7 +10,7 @@ const VERSION = ACTIVE_OMNIGRAPH_VERSION.replace(/^v/, ""); ---