diff --git a/.prettierrc.json b/.prettierrc.json index dbaacb633e..f703aa8dd9 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -12,8 +12,8 @@ } }, { - "comment": "Walkthroughs mirror the example apps verbatim; don't reflow their embedded GraphQL.", - "files": ["docs/ensnode.io/src/components/walkthroughs/**/*.mdx", "examples/**/*.{ts,tsx}"], + "comment": "Don't reflow embedded code: hand-authored GraphQL examples throughout the docs, and the example apps that walkthroughs mirror verbatim.", + "files": ["docs/ensnode.io/**/*.{md,mdx}", "examples/**/*.{ts,tsx}"], "options": { "embeddedLanguageFormatting": "off" } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 699ed73319..a729da5e9f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["biomejs.biome"] + "recommendations": ["biomejs.biome", "esbenp.prettier-vscode"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index f2bf775887..fd3f6f1d69 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,5 +29,14 @@ }, "[terraform]": { "editor.defaultFormatter": "hashicorp.terraform" + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[mdx]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[astro]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts index becf6b78e1..a4df87191b 100644 --- a/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts +++ b/docs/ensnode.io/config/integrations/starlight/sidebar-topics/integrate.ts @@ -29,7 +29,7 @@ export const integrateSidebarTopic = { link: "/docs/integrate/ens-subgraph", }, { - label: "Key Limitations", + label: "Key Limitations 🚨", link: "/docs/integrate/ens-subgraph/key-limitations", }, { @@ -72,6 +72,10 @@ export const integrateSidebarTopic = { label: "Overview", link: "/docs/integrate/omnigraph", }, + { + label: "Core Concepts", + link: "/docs/integrate/omnigraph/concepts", + }, { label: "Protocol Acceleration", link: "/docs/integrate/omnigraph/protocol-acceleration", 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 c4b01bf3a6..96c378c3b5 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 @@ -1,5 +1,4 @@ import { LinkCard, CardGrid, Aside } from "@astrojs/starlight/components"; -import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanceSdkVersionWarning.astro"; import OmnigraphStaticExampleSet from "@components/organisms/OmnigraphStaticExampleSet.astro"; ## What is ENSv2? @@ -30,8 +29,6 @@ Here's a summary of some popular integration strategies: 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 title="example.ts" // create and extend an EnsNodeClient with Omnigraph API support const client = createEnsNodeClient({ url: process.env.ENSNODE_URL! }).extend(omnigraph); @@ -72,76 +69,34 @@ const result = await client.omnigraph.query({ query: HelloWorldQuery }); 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 title="example.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( - ` +// this query is fully typechecked and supports editor autocomplete! +const DomainByNameQuery = graphql(` query DomainByNameQuery($name: InterpretedName!) { domain(by: { name: $name }) { - ...DomainFragment - subdomains { - edges { node { ...DomainFragment } } + name + owner { + address } } } -`, - [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 }) { +export function DomainCard({ 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 (fetching) return

Loading...

; if (error) return

Error: {error.message}

; - if (!data?.domain) return

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

; + if (!data?.domain) return

No domain found for '{name}'.

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

Subdomains:

-
    - {subdomains?.edges.map((edge) => { - const { id } = readFragment(DomainFragment, edge.node); - return ( -
  • - -
  • - ); - })} -
+

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

+

Owner: {domain.owner?.address ?? "Unowned"}

); } diff --git a/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.15.0.mdx b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.15.0.mdx index 23b6683565..73aa3c0409 100644 --- a/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.15.0.mdx +++ b/docs/ensnode.io/src/components/walkthroughs/quickstart/v1.15.0.mdx @@ -1,5 +1,4 @@ import { LinkCard, CardGrid, Aside } from "@astrojs/starlight/components"; -import HostedInstanceSdkVersionWarning from "@components/molecules/HostedInstanceSdkVersionWarning.astro"; import OmnigraphStaticExampleSet from "@components/organisms/OmnigraphStaticExampleSet.astro"; ## What is ENSv2? @@ -30,8 +29,6 @@ Here's a summary of some popular integration strategies: 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 title="example.ts" // create and extend an EnsNodeClient with Omnigraph API support const client = createEnsNodeClient({ url: process.env.ENSNODE_URL! }).extend(omnigraph); @@ -40,7 +37,6 @@ const client = createEnsNodeClient({ url: process.env.ENSNODE_URL! }).extend(omn const HelloWorldQuery = graphql(` query HelloWorld { domain(by: { name: "eth" }) { - id canonical { name { beautified } } owner { address } } @@ -72,76 +68,32 @@ const result = await client.omnigraph.query({ query: HelloWorldQuery }); 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 title="example.tsx" -// this is fully typechecked and supports editor autocomplete! -const DomainFragment = graphql(` - fragment DomainFragment on Domain { - __typename - id - canonical { name { beautified } } - owner { id address } - } -`); - -// this is fully typechecked and supports editor autocomplete! -const DomainByNameQuery = graphql( - ` +// this query is fully typechecked and supports editor autocomplete! +const DomainByNameQuery = graphql(` query DomainByNameQuery($name: InterpretedName!) { domain(by: { name: $name }) { - ...DomainFragment - subdomains { - edges { node { ...DomainFragment } } - } + canonical { name { beautified } } + owner { address } } } -`, - [DomainFragment], -); - -function RenderDomainFragment({ data }: { data: FragmentOf }) { - // type-safe access to fragment data! - const domain = readFragment(DomainFragment, data); - - return ( - <> - Name: {domain.canonical ? domain.canonical.name.beautified : "Unnamed Domain"} - Protocol Version: {domain.__typename === "ENSv1Domain" ? "ENSv1" : "ENSv2"} - Owner: {domain.owner ? domain.owner.address : "Unowned"} - - ); -} +`); -export function RenderDomainAndSubdomains({ name }: { name: InterpretedName }) { +export function DomainCard({ 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 (fetching) return

Loading...

; if (error) return

Error: {error.message}

; - if (!data?.domain) return

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

; + if (!data?.domain) return

No domain found for '{name}'.

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

Subdomains:

-
    - {subdomains?.edges.map((edge) => { - const { id } = readFragment(DomainFragment, edge.node); - return ( -
  • - -
  • - ); - })} -
+

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

+

Owner: {domain.owner?.address ?? "Unowned"}

); } diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/ens-subgraph/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/ens-subgraph/index.mdx index 1ecb353dcc..75d4cc0479 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/ens-subgraph/index.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/ens-subgraph/index.mdx @@ -8,14 +8,14 @@ sidebar: import { LinkCard } from "@astrojs/starlight/components"; -:::danger[The ENS Subgraph is legacy β€” do not build new ENS apps on it!] -The ENS Subgraph predates ENSv2 and **cannot** carry your app into it. Three reasons it falls short: +:::danger[🚨 The ENS Subgraph is not ENSv2 compatible] +The ENS Subgraph **fundamentally fails as a source of ENS data as soon as ENSv2 launches.** -- **ENSv1 only β€” instantly out of date.** The Subgraph data model has no concept of ENSv2. The moment ENSv2 launches (Summer 2026), apps still reading the Subgraph are looking at a stale, partial view of ENS. -- **Single-chain only β€” misses most names.** The Subgraph indexes a single chain, so it never sees Basenames (`.base.eth`), Lineanames (`.linea.eth`), or 3DNS names (`.box`). -- **No resolution β€” and slow when you bolt it on.** The Subgraph doesn't resolve names; you're left making your own RPC calls, which are slow, hard to batch, and easy to get wrong. - -These are only the headline issues. See the full list of [Key Limitations](/docs/integrate/ens-subgraph/key-limitations). + ::: :::tip[Start here instead: the ENS Omnigraph API] diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/ens-subgraph/key-limitations.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/ens-subgraph/key-limitations.mdx index 1c8bcedf85..3fd9c3307f 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/ens-subgraph/key-limitations.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/ens-subgraph/key-limitations.mdx @@ -18,19 +18,22 @@ The [ENS Omnigraph API](/docs/integrate/omnigraph) is built to close every gap o ## Two systems, neither complete +:::danger[DIY ENS Integrations are Hard] Historically, full access to ENS data required two separate data-fetching strategies working in parallel: 1. **ENS resolution** β€” RPC calls with CCIP-Read support for offchain data (e.g. via `viem` or `wagmi`) to perform forward or reverse resolution. 2. **Indexed ENS data** β€” the ENS Subgraph, for discovering names owned by an address and all other ENS state outside of resolution. Neither system alone is complete. Resolution gives you resolver records but no access to the rest of ENS state, and it is painfully "close to the metal." The Subgraph gives you queryable indexed data but cannot resolve names and carries the limitations below. Apps have had to live with the split, its limitations, and its downstream complexity β€” and **with ENSv2, the complexity of ENS's onchain state meaningfully increases.** +::: -:::tip +:::tip[One unified API] The [Omnigraph API](/docs/integrate/omnigraph) bundles access to ENS onchain resources (like Domains and Registrations) with Protocol Accelerated Resolution; a single unified API for all of your ENS needs. ::: ## No ENS resolution β€” and apps that fake it are broken +:::danger[Faking resolution ships real bugs] The Subgraph does not perform ENS resolution. It has no concept of the ENS Universal Resolver, CCIP-Read, or ENSIP-10 wildcard resolution. Despite this, developers routinely reach for the Subgraph to resolve names β€” because it's the indexed data source already in front of them β€” which produces incorrect results because it **doesn't follow the ENS Forward Resolution protocol**. This isn't hypothetical. It happens in widely-used software: @@ -40,93 +43,117 @@ This isn't hypothetical. It happens in widely-used software: - **[Ethereum Comments Protocol](https://github.com/ecp-eth/comments-monorepo)** resolves names [against an indexer query](https://github.com/ecp-eth/comments-monorepo/blob/c301d76fa56b6b807f135c98273ac9eb5ddebe95/apps/indexer/src/services/resolvers/ens-by-query-resolver.ts) rather than the resolution protocol. Each of these approaches produces results that diverge from what the ENS protocol _actually_ says. +::: -:::tip +:::tip[Protocol-correct resolution] The [Omnigraph API](/docs/integrate/omnigraph) performs protocol-correct resolution for you β€” including the CCIP-Read offchain lookups β€” so the correct result is the default. ::: ## It forces you to stitch together multiple APIs +:::danger[You become the integration glue] Because the Subgraph can't resolve names, any app that needs both indexed data _and_ resolution has to run two integrations side by side: the Subgraph for indexed state, and a resolution library for records. You reconcile their differences, their failure modes, and their data models yourself. Developers shouldn't have to care about these implementation details of the ENS protocol β€” getting "all the ENS data I need," whether ENSv1, ENSv2, indexed, or resolved, should come from a single unified API. +::: -:::tip -The [Omnigraph API](/docs/integrate/omnigraph) provides a unified datamodel across ENSv1 and ENSv2 resources: write your query once and your platform automatically understands both protocol versions without any extra work on your end. +:::tip[Write your query once] +The [Omnigraph API](/docs/integrate/omnigraph) provides a unified datamodel across ENSv1 and ENSv2: write your query once and your platform automatically understands both protocol versions without any extra work on your end. ::: ## ENSv1 only β€” blind to ENSv2 +:::danger[Stale the moment ENSv2 launches] The Subgraph's data model has no concept of ENSv2. The moment ENSv2 launches (Summer 2026), apps still reading the Subgraph are looking at a stale, partial view of ENS β€” missing the new Namegraph data model entirely. There is no upgrade path: the schema was never designed for it. +::: + +:::tip[The Omnigraph transparently upgrades to ENSv2] +The [Omnigraph API](/docs/integrate/omnigraph) provides a unified datamodel across ENSv1 and ENSv2: your app works before, during, and after the ENSv2 release without any changes. +::: ## Single-chain only β€” misses most names +:::danger[Most ENS names are invisible] The Subgraph indexes a single chain, so it never sees Basenames (`.base.eth`), Lineanames (`.linea.eth`), or 3DNS names (`.box`). A large and growing majority of ENS names already live off of mainnet and are simply invisible to it. +::: -:::tip +:::tip[Every chain in one schema] The [Omnigraph API](/docs/integrate/omnigraph) indexes the full suite of onchain ENS names, including Basenames (`.base.eth`), Lineanames (`.linea.eth`) and 3DNS names (`.box`). ::: ## No multichain primary names (ENSIP-19) +:::danger[No cross-chain primary names] Beyond being single-chain, the Subgraph has no concept of [ENSIP-19](https://docs.ens.domains/ensip/19) multichain primary names. Even an app willing to query several per-chain Subgraphs cannot reconstruct a name's primary-name configuration across chains from Subgraph data. +::: -:::tip +:::tip[Full ENSIP-19 support] The [Omnigraph API](/docs/integrate/omnigraph) fully implements [ENSIP-19](https://docs.ens.domains/ensip/19) and accurately returns an account's multichain primary names in milliseconds. ::: ## No concept of the effective resolver (ENSIP-10) +:::danger[Wrong resolver, wrong records] The Subgraph records the resolver _assigned_ to a domain but has no understanding of [ENSIP-10](https://docs.ens.domains/ensip/10) wildcard resolution, and therefore no concept of the _effective_ resolver β€” the resolver that actually answers for a name via a parent's wildcard resolver. Apps that read the assigned resolver from the Subgraph and assume it's the effective one get the wrong answer for any name that relies on wildcard resolution. +::: -:::tip +:::tip[Assigned and effective resolvers] The [Omnigraph API](/docs/integrate/omnigraph) supports both a Domain's _assigned_ resolver as well as its [ENSIP-10](https://docs.ens.domains/ensip/10) _effective_ resolver, so you can write applications that understand the difference, whether you're resolving up-to-date records or letting users edit records onchain. ::: ## Unnormalized names (ENSIP-15 not applied) -The Subgraph does not apply [ENSIP-15](https://docs.ens.domains/ensip/15) name normalization. It returns label and name values as indexed, including unnormalized ones, with no signal about whether a value is normalized. Every consuming app has to implement normalization correctly and consistently β€” and the ones that don't display or match names incorrectly. ENSNode can [replace unnormalized labels](/docs/integrate/ens-subgraph/backwards-compatibility#never-normalize-labels-returned-by-ensnode) for safer handling. +:::danger[Normalization is left to you] +The Subgraph does not apply [ENSIP-15](https://docs.ens.domains/ensip/15) name normalization. It returns unnormalized labels and names, putting the burden on every consuming app to implement normalization correctly and consistently β€” and the ones that don't display or match names incorrectly. ENSNode [replaces unnormalized labels](/docs/integrate/ens-subgraph/backwards-compatibility#never-normalize-labels-returned-by-ensnode) for you, so you automatically enjoy safer handling. +::: -:::tip +:::tip[Interpreted Names by default] The [Omnigraph API](/docs/integrate/omnigraph) stores and operates over [Interpreted Names](/docs/reference/terminology#interpreted-name), a consistent name format that ensures that names are composed of labels that are either normalized or [Encoded LabelHashes](/docs/reference/terminology#encoded-labelhash). This means consistent handling and fewer application bugs. ::: ## Unstable domain identification +:::danger[Identifiers shift underneath you] Labels in the Subgraph are not stable identifiers. A label that is "unknown" today can become "known" later (as label-healing coverage grows), and the set of normalizable names can change over time. Apps that key on label or name strings will see identifiers shift underneath them. The only stable identifier is the `node` (the namehash of the name) β€” but the Subgraph schema surfaces it as the `id` field, and getting this right requires careful, documented handling. See [Use the node as the stable identifier](/docs/integrate/ens-subgraph/backwards-compatibility#use-the-node-as-the-stable-identifier). +::: -:::tip +:::tip[Stable IDs that never move] The [Omnigraph API](/docs/integrate/omnigraph) provides stable identification via a Domain's `id`, a multichain-aware globally unique identifier that works for both ENSv1 and ENSv2 Domains. Domains are also addressable by [InterpretedName](/docs/reference/terminology#interpreted-name), and a Domain's Canonical Name (`Domain.canonical.name`) is always maximally healed at request time, thanks to [ENSRainbow](/docs/services/ensrainbow). ::: ## Effective ownership is hard to determine +:::danger[Effective ownership is ambiguous] The Subgraph schema spreads ownership across multiple fields β€” `owner`, `registrant`, `wrappedOwner` β€” reflecting raw protocol state (the Registry, the `.eth` Registrar, and the Name Wrapper). Determining the _effective_ owner of a domain requires understanding the interplay of all of them. This is exactly the kind of protocol-implementation detail app developers are forced to learn and re-implement, with plenty of room to get it wrong. +::: -:::tip +:::tip[One effective owner field] In the [Omnigraph API](/docs/integrate/omnigraph) `Domain.owner` is _always_ the effective owner's addressβ€”no weird edge-cases! For ENSv2 Domains, `Domain.owner` is Smart-Account-aware and represents the true owner of the Domain at a given time. ::: ## Missing all offchain ENS names +:::danger[Offchain names are missing entirely] The Subgraph indexes onchain events only, so it has no knowledge of offchain ENS names. A meaningful and growing slice of ENS lives offchain and is entirely absent from Subgraph data. +::: -:::tip +:::tip[Automatic offchain name resolution] While the [Omnigraph API](/docs/integrate/omnigraph) doesn't (_yet!_) index offchain names, it does provide protocol-correct Accelerated Forward Resolution, including support for offchain CCIP-Read-based names. ::: ## Raw "bare-metal" values push the decoding burden onto you +:::danger[Decoding is your problem] The Subgraph exposes raw values straight from the ENS protocol, with none of the interpretation that apps actually need: - **Address records** may be for non-EVM chains and need chain-specific decoding before they're usable. - **Contenthash** values are encoded and need decoding to become a usable URL. -- **Avatar records** are frequently NFT references or `ipfs://` / `ar://` URIs β€” not something a browser `` can render without resolution. - **Text records** are represented by users in many inconsistent ways. Consider the many variations in how someone might set a Twitter/X handle β€” both the record key and the value vary widely. Every bit of this interpretation, decoding, and normalization is left to the app developer. The result is more bugs in ENS integrations across the ecosystem, which damages the network effects and growth of ENS. +::: -:::tip[Coming Soon 🚧] +:::tip[Interpreted records β€” coming soon 🚧] The [Omnigraph API](/docs/integrate/omnigraph) will support two major record resolution use-cases: 1. Protocol-accurate 'raw' requests, without post processing, and @@ -136,38 +163,47 @@ The [Omnigraph API](/docs/integrate/omnigraph) will support two major record res ## It can't cleanly power NFT-reference β†’ avatar use cases +:::danger[Avatar resolution is painful] A common and important flow is taking an NFT reference as input and mapping it: **NFT Ref β†’ Domain β†’ Name β†’ Avatar text record β†’ Avatar image.** This powers services like the ENS Metadata Service, which provides NFT metadata for ENS names using standardized protocols adopted by platforms such as OpenSea, Rarible, Grails, and ENS Vision. The Subgraph's raw, resolution-free data model makes this flow far harder than it should be. +::: -:::tip[Coming Soon 🚧] +:::tip[Automatic avatars β€” coming soon 🚧] The [Omnigraph API](/docs/integrate/omnigraph) will support automatic avatar url derivation; all your app will need to do is render an `` tag! ::: ## Unhealed names degrade developer and user experience +:::danger[Unhealed labelhashes everywhere] The Subgraph (and any data-level-compatible indexer) contains a large volume of unhealed names β€” names whose labels are only known as labelhashes (e.g. `[abcd…].eth`). These complicate both the developer experience and the UX of every app built on top, which must decide how to display and handle them. +::: -:::tip +:::tip[~94% healed, and climbing] The [Omnigraph API](/docs/integrate/omnigraph) already heals **~94%** of these labels, compared to roughly **10%** with the label set bundled in the traditional ENS Subgraph β€” and climbing toward a **99%** target. ::: ## Thick-client lock-in +:::danger[Locked into a thick TypeScript client] Achieving common ENS query operations correctly against the Subgraph requires meaningful additional logic. In practice that logic lives in "thick client" libraries β€” most notably `ensjs`, which embeds special-case handling for working with the Subgraph (e.g. decoding names, reconstructing histories, assembling the queries for names owned by an address). A thin client that simply wraps the GraphQL API can't reproduce these behaviors. And because such thick clients are written for a single language (`ensjs` is TypeScript only), developers building on ENS in any other language don't get them at all. +::: -:::tip +:::tip[Any client, any language] The [Omnigraph API](/docs/integrate/omnigraph) is a Relay-compatible GraphQL API; bring your own GraphQL client library, or simply make GraphQL requests over HTTP and receive exactly the data you need! ::: ## No type-safe API client +:::danger[No type safety, no autocomplete] The Subgraph's auto-generated GraphQL API ships without a first-class, type-safe client. Developers are left hand-writing queries and types, with no compile-time guarantees that a query matches the schema or that responses are shaped as expected. +::: -:::tip +:::tip[Typed enssdk and enskit] The [Omnigraph API](/docs/integrate/omnigraph) comes with [`enssdk` (TypeScript)](/docs/integrate/integration-options/enssdk) and [`enskit` (React)](/docs/integrate/integration-options/enskit) fully typed client libraries with built-in editor intellisense and autocomplete. ::: ## Operational gaps +:::danger[Operations left half-solved] Beyond the data-model limitations, the Subgraph's auto-generated GraphQL API leaves common operational needs underserved: - **Pagination** is awkward and easy to implement incorrectly at scale. @@ -175,7 +211,9 @@ Beyond the data-model limitations, the Subgraph's auto-generated GraphQL API lea - **Indexing status** is hard to reason about β€” there's no clear, app-friendly signal for how far behind realtime the data is (e.g. worst-case distance from the chain head). - **Joins** in the auto-generated API perform certain multi-entity operations in ways that don't match what apps actually need, forcing client-side stitching. -:::tip +::: + +:::tip[Relay pagination and caching] The [Omnigraph API](/docs/integrate/omnigraph) supports [Relay Connections](https://relay.dev/graphql/connections.htm) for paginated resources; your app gets lightning-quick infinite scroll with minimal work! If using [`enskit` (React)](/docs/integrate/integration-options/enskit), the `useOmnigraphQuery` hook comes with Omnigraph-specific local cache directives for instant resolution of cacheable values. The Omnigraph's idiomatic GraphQL API also aims to match consumer query patterns as closely as possible to negate complex and bespoke client-side logic; if your use-case isn't yet supported, [open an issue on GitHub](https://github.com/namehash/ensnode/issues/new). ::: diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/ensv2-readiness.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/ensv2-readiness.mdx index 72b97d0c48..82f38dd0d8 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/ensv2-readiness.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/ensv2-readiness.mdx @@ -22,9 +22,9 @@ Full access to ENS data formerly required two separate data-fetching strategies ## How ENSNode solves this -**The Unigraph Data Model** β€” [ENSIndexer](/docs/services/ensindexer) offers the `unigraph` plugin, which builds a **unified** polymorphic indexed data model in [ENSDb](/docs/services/ensdb) of multiple ENSv1 Nametrees (the ENSv1 onchain state model found in the ENS root registry, Basenames on Base, Lineanames on Linea, 3DNS on Optimism, etc.), and the ENSv2 Namegraph (the ENSv2 onchain state model that ENSv1 domains may optionally be upgraded to, which allows for dynamic reconfiguration of the name hierarchy, including possible cycles, disjoint domains, and infinite aliases). +**The Unigraph Data Model** β€” [ENSIndexer](/docs/services/ensindexer)'s `unigraph` plugin builds a single, **unified** indexed data model in [ENSDb](/docs/services/ensdb) that combines all of ENSv1 β€” mainnet `.eth`, Basenames on Base, Lineanames on Linea, 3DNS on Optimism β€” together with ENSv2. One data model, every chain, both protocol versions. -**The Omnigraph API** β€” The [ENS Omnigraph API](/docs/integrate/omnigraph) is delivered by [ENSApi](/docs/services/ensapi) and builds upon and refines the Unigraph data model held in [ENSDb](/docs/services/ensdb). The Omnigraph API provides a highly tailored, fully typed GraphQL API that understands ENS protocol implementation detailsβ€”including ENSIP-10, ENSIP-15, ENSIP-19, and much moreβ€”so that you can focus on building your app (and not all the internal implementation complexities of the ENS protocol). +**The Omnigraph API** β€” The [ENS Omnigraph API](/docs/integrate/omnigraph), delivered by [ENSApi](/docs/services/ensapi), is a fully typed GraphQL API on top of that Unigraph data model. It handles the ENS protocol's many implementation details for you, so you can focus on building your app instead of wiring up the protocol's internals. **Accelerated ENS Resolution** β€” The ENS Omnigraph API, through ENSApi, internally implements the ENS Universal Resolver on top of the indexed ENS Unigraph data model in ENSDb. We refer to this idea as "[ENS Protocol Acceleration](/docs/integrate/omnigraph/protocol-acceleration)". For cases where ENS Resolution requires offchain data, ENSApi internally performs the CCIP-read operations on your behalf to ensure every resolution request accurately follows all ENS protocol standards. No need for any RPC calls in your app, and your ENS resolutions for indexed names could speed up by an order of magnitude or more! diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/concepts.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/concepts.mdx new file mode 100644 index 0000000000..9aca483282 --- /dev/null +++ b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/concepts.mdx @@ -0,0 +1,494 @@ +--- +title: ENS Omnigraph Core Concepts +description: The core data-model concepts behind the ENS Omnigraph API β€” Namegraph, Unigraph, canonicality, polymorphism, Interpreted Names, Events, and Permissions. +sidebar: + label: Core Concepts +--- + +import { CardGrid, LinkCard, Aside } from "@astrojs/starlight/components"; + +While the [ENS Omnigraph API](/docs/integrate/omnigraph) abstracts away most of the complexity of the ENS protocol, there are still some core concepts that are useful to learn. These concepts will help you understand _how_ the Omnigraph presents ENSv1 and ENSv2 through a single unified schema. You don't need to master these concepts to build your first Omnigraph query, but as you level-up to building more advanced ENS features in your app, these concepts will help you to understand how the Omnigraph represents the state of the ENS protocol. + + + +## What is a Namegraph? + +A **Namegraph** is the native onchain data model of ENSv2: it represents names not as a flat mapping of namehashes in a Nametable (as in ENSv1), but as a graph of `Registry β†’ Domain β†’ Registry β†’ Domain β†’ …`. This graph may be cyclic, and within the ENSv2 protocol an unbounded number of _disjoint_ (not connected) Namegraphs will exist. Within the unified ENS protocol (v1+v2) there will exist many Namegraphs, at the very least those headed by the ENSv1 Root Registry, Basenames, Lineanames, 3DNS, and the ENSv2 Root Registry. + +Because ENSv1 names do not actually have on-chain Subregistries, the Unigraph represents this relationship with an `ENSv1VirtualRegistry`. + +```mermaid +flowchart TD + root([ENSv1 Root Registry]) + eth["eth"] + ethReg([".eth (virtual) Subregistry"]) + vitalik["vitalik.eth"] + exampleName["example.eth"] + vitalikReg(["vitalik.eth's (virtual) Subregistry"]) + blog["blog.vitalik.eth"] + exampleReg(["example.eth's (virtual) Subregistry"]) + sub["sub.example.eth"] + + root --> eth + eth --> ethReg + ethReg --> vitalik + ethReg --> exampleName + vitalik --> vitalikReg + vitalikReg --> blog + exampleName --> exampleReg + exampleReg --> sub +``` + +You navigate the graph by following a Domain to its Subregistry to its child Domains, and so on: + +```graphql title="example.gql" +query Namegraph { + # reference a Domain by name + domain(by: { name: "eth" }) { + # this is the Registry that "eth" exists within + registry { id contract { chainId address } } + + # "eth"'s parent Domain (if any) + parent { id } + + # the Subregistry that "eth" declares + subregistry { + domains { + edges { + node { + # get each domain's (beautified) name + canonical { name { beautified } } + } + } + } + } + + # Domain.subdomains is short form of Domain.subregistry.domains + subdomains { edges { node { canonical { name { beautified } } } } } + } +} +``` + +## What is the Unigraph? + +The **Unigraph** is the entire collection of these disjoint ENSv2 Namegraphs and multiple ENSv1 Nametables, combined together into a single unified data model using ENS Resolution semantics. Navigating the Unigraph from `"eth"` down to `"vitalik.eth"` and beyond looks identical regardless of whether the underlying entities are ENSv1 or ENSv2. + +The [`unigraph` plugin](/docs/services/ensindexer) in ENSIndexer is what builds this unified model. The Unigraph constructs two Namegraphs, one rooted at the ENSv1 Root Registry and another rooted at the ENSv2 Root Registry. It's also where multichain coverage lives: Basenames (`.base.eth`), Lineanames (`.linea.eth`), and 3DNS names (`.box`) are all stitched into the ENSv1 Namegraph. + +```mermaid +flowchart TD + root([ENSv1 Root Registry]) + eth["eth"] + ethReg([.eth Subregistry]) + vitalik["vitalik.eth"] + baseName["base.eth"] + vitalikReg([vitalik.eth Subregistry on Ethereum]) + blog["blog.vitalik.eth"] + baseReg([Basenames Subregistry on Base Chain]) + jesse["jesse.base.eth"] + + root --> eth + eth --> ethReg + ethReg --> vitalik + ethReg --> baseName + vitalik --> vitalikReg + vitalikReg --> blog + baseName --> baseReg + baseReg --> jesse +``` + +The same query shape works for any indexed name regardless of chain or protocol version β€” here, a Basename on Base: + +```graphql title="example.gql" +query Basenames { + domain(by: { name: "jesse.base.eth" }) { + canonical { name { interpreted } } + } +} +``` + +## Unigraph with ENSv2 + +Once ENSv2 launches, the ENSv2 Namegraph will exist in parallel with the ENSv1 Namegraph. When referencing a Domain by `name`, the Omnigraph will start at the **ENSv2 Root Registry**, and traverse the Namegraph to find the appropriate Domain. Once `.eth` names are reserved in the ENSv2 `EthRegistry`, then the **ENSv2 Domain** will be returned, since that's the Domain that would be referenced during resolution. This is part of why it's important to reference specific Domains by `id`; once `vitalik` has been reserved in the ENSv2 EthRegistry, the ENS protocol considers the ENSv2 Domain (_not_ the ENSv1 Domain) to be the 'real' one. That said, after the `.eth` names are reserved (but before they're individually migrated), their resolver will be the `ENSv1Resolver`, forwarding resolution to the ENSv1 Namegraph. + +The end result is that there are **two** Domains considered to be "vitalik.eth", one in the ENSv1 Namegraph and one in the ENSv2 Namegraph. + +```graphql title="example.gql" +query ByProtocolVersion { + # before ENSv2 launches: returns ENSv1 vitalik.eth + # after ENSv2 launched: returns ENSv2 vitalik.eth + domain(by: { name: "vitalik.eth" }) { id } + + # always returns the protocol-specific version of vitalik.eth + domains(where: { name: { eq: "vitalik.eth" }, version: ENSv1 }) { + edges { node { id } } + } +} +``` + +:::note[Domain `id` vs `name`] +This is part of why there's a distinction between a Domain's `id` (the stable, unique reference that always refers to the same onchain entity) and a Domain's `name` (which may change over time). See below for further discussion. +::: + +## Canonicality + +Given that a Domain entity (say, the `sub` in `sub.example.eth`) can be reached by infinitely many aliases (for example, `sub.other.eth`), it becomes important to determine a _canonical_ reference to the Domain β€” this is the **Canonical Name**. Canonicality is also connected to nameability within the Unigraph; if an ENSv2 Domain exists on-chain but isn't eventually connected to the ENSv1 or ENSv2 Root Registry via a series of canonical names, **it doesn't have a Canonical Name!** + +Within the Omnigraph API the complexity of the Namegraph is reduced, and all Canonical Domains are queryable, searchable, and addressable by said Canonical Name. Domains that are not canonical are _still_ referenceable by `id` (eg. `domain(by: { id: DomainId! })`). + +Canonical Domains have a `Domain.canonical` field hosting the canonicality-derived fields such as `name`, `node`, `depth` (i.e. 2 for `vitalik.eth`), and `path` from the ENS root. + +```graphql title="example.gql" +query Canonicality { + domain(by: { name: "vitalik.eth" }) { + canonical { + name { + interpreted # the InterpretedName + beautified # the ENSIP-15 BeautifiedName + } + node # namehash(name) + depth # i.e. 2 for "vitalik.eth" + path { id } # [Domain("eth"), Domain("vitalik.eth")] + } + } +} +``` + +## Stable IDs vs. Namegraph addressing + +Every entity in the Omnigraph has an `id` β€” a nominally-typed, stable reference to a specific on-chain entity (`DomainId`, `RegistryId`, `RegistrationId`, etc.). + +For Domains, when you already have an `id` and want to reference the _exact same_ on-chain entity, query it by `id`: `domain(by: { id: "..." })` which is stable across time, even if its Canonical Name could change. + +Addressing a Domain by **name** is a different operation. It's **forward traversal of the unified Namegraph**: `domain(by: { name: "vitalik.eth" })` walks from the Root Registry (ENSv1 or ENSv2 if defined) β†’ `"eth"` in that Registry β†’ the Registry that `"eth"` points at (the EthRegistry) β†’ `"vitalik"` in that Registry. The Domain returned is whichever on-chain entity (if any) would be identified during Forward Resolution. + +These two views are not interchangeable: + +- The `id` you receive from a name lookup _is_ the stable reference to the on-chain entity that the Namegraph currently resolves `"vitalik.eth"` to. +- But `"vitalik.eth"` is not a stable reference to that entity. The Namegraph can be re-parented or re-aliased β€” and tomorrow, `"vitalik.eth"` may resolve to an entirely different on-chain Domain, which may have a different resolver with different records. + +Address the exact on-chain entity by `id` β€” stable across time: + +```graphql title="example.gql" +query ById { + domain(by: { id: "..." }) { + canonical { name { interpreted } } + } +} +``` + +Address by `name` to perform forward traversal β€” you get whichever entity the name resolves to right now: + +```graphql title="example.gql" +query ByName { + domain(by: { name: "vitalik.eth" }) { + __typename # could be ENSv1Domain or ENSv2Domain + id # vitalik.eth could refer to a different Domain over time, but id is always stable + canonical { name { interpreted } } + } +} +``` + +:::note[Rule of Thumb] +Address by `name` when you're answering "which Domain would records come from if resolved right now"; address by `id` when you're answering "what's the latest state of this specific on-chain entity?". + +So if you're writing an application that shows a profile for `vitalik.eth`, `by: { name: "vitalik.eth" }`. If you're writing an application where users are managing their on-chain Registry contracts and the Domains they own, reference each `by: { id: "..." }`. +::: + +## Polymorphism via GraphQL interfaces + +`Domain`, `Registry`, and `Registration` are GraphQL **interfaces**, with concrete types implementing each: + +- `Domain` β†’ `ENSv1Domain`, `ENSv2Domain` +- `Registry` β†’ `ENSv1Registry`, `ENSv1VirtualRegistry`, `ENSv2Registry` +- `Registration` β†’ `BaseRegistrarRegistration`, `NameWrapperRegistration`, `ThreeDNSRegistration`, `ENSv2RegistryRegistration`, `ENSv2RegistryReservation` + +Shared fields are available unconditionally on the interface. Protocol- or implementation-specific fields are reached via typed inline fragments β€” `... on ENSv1Domain { rootRegistryOwner }`, `... on ENSv2Domain { tokenId }`, `... on BaseRegistrarRegistration { wrapped { fuses } }`, `... on NameWrapperRegistration { fuses }`. The result is a single query that compiles, type-checks, and returns the right fields for whichever concrete type each record turns out to be. + +```graphql title="example.gql" +query Polymorphism { + domain(by: { name: "vitalik.eth" }) { + __typename + + ... on ENSv1Domain { + # the owner of the Domain in the ENSv1 Root Registry + rootRegistryOwner { address } + } + + ... on ENSv2Domain { + # ENSv2 Domains are identified by a `tokenId` within their Registry + tokenId + } + } +} +``` + +## InterpretedNames and InterpretedLabels everywhere + +Every name and label crossing the Omnigraph surface is an **Interpreted Name** (or **Interpreted Label**). Each label in an Interpreted Name is either a normalized literal label or an Encoded LabelHash (`[abc123…]`) when the literal isn't known or is unnormalized. This eliminates one of the most common ENS UI footguns β€” unnormalized labels, unhealed hashes, and rendering surprises β€” at the schema layer, making UI rendering trivial. See [terminology](/docs/reference/terminology#interpreted-label) for the full definition. + +In addition, both provide a `beautified` variant, where the [ENSIP-15](https://docs.ens.domains/ensip/15) beautified form is rendered. This allows your UI to trivially render the best form of a name or label, without further logic. + +```graphql title="example.gql" +query InterpretedNames { + domain(by: { name: "vitalik.eth" }) { + label { + interpreted # "vitalik" + beautified # "vitalik" + hash # 0xaf2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103cc + } + + canonical { + name { + interpreted # "vitalik.eth" + beautified # "vitalik.eth" + } + } + } +} +``` + +```tsx title="Example.tsx" +

the Label for this domain is {domain.label.beautified}

+``` + + + + + + + + +### InterpretedName with Encoded LabelHash Example + +As noted above, an InterpretedName may contain Labels that are [Encoded LabelHashes](/docs/reference/terminology#encoded-labelhash), meaning that the human-readable Label isn't known or isn't normalized. The Omnigraph still supports referencing Domains by these InterpretedNames, like so: + +```graphql title="example.gql" +query ByInterpretedName { + domain(by: { name: "[af2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103cc].eth" }) { + id # this is the same Domain as "vitalik.eth" above! + canonical { + name { + # in this case, the Omnigraph knows the fully healed Canonical Name of "vitalik.eth" + interpreted # "vitalik.eth" + } + } + } +} +``` + +:::danger[Not all InterpretedNames are Resolvable] +ENS Forward Resolution does _not_ support Encoded LabelHashes in names, so while an InterpretedName can be used to traverse the Namegraph (i.e. with `domain(by: { name: "" })`), if the Canonical Name is not a [ResolvableName](/docs/reference/terminology#resolvable-name), then the records for that name _cannot_ be determined. +::: + +### BeautifiedNames and BeautifiedLabels + +For some labels and names, there exists an [ENSIP-15](https://docs.ens.domains/ensip/15) beautified form, where certain normalized characters are 'beautified' into their _un-normalized_ but _normalizable_ prettier forms. + +For example, the InterpretedName `β™Ύ.eth` (normalized) would be beautified to `♾️.eth` (un-normalized, but is normalizable back to `β™Ύ.eth`). All name and label fields in the Omnigraph provide a `beautified` variant which can be used for display. + +```graphql title="example.gql" +query Beautified { + domain(by: { name: "β™Ύ.eth" }) { + canonical { + name { + interpreted # β™Ύ.eth + beautified # ♾️.eth + } + } + } +} +``` + +```tsx title="Example.tsx" +

the Domain's name is {domain.canonical?.name.beautified ?? 'unknown'}

+``` + +:::danger[Only use BeautifiedNames for Display] +[BeautifiedNames](/docs/reference/terminology#beautified-name) are only suitable for display, not for identification of a Domain. Always use a Domain's `id` or [InterpretedName](/docs/reference/terminology#interpreted-name) as an identifier (for url paths, for example). +::: + +## Relay-spec connections + +Collection and paginated relationship fields in the schema follow the [Relay-spec Connection](https://relay.dev/graphql/connections.htm) pattern with `edges`, `pageInfo`, and `totalCount`. Cursor-based pagination is idiomatic in urql, Apollo, Relay, and most modern GraphQL clients β€” infinite scroll and stable pagination work out of the box, with no per-endpoint plumbing. + +```graphql title="example.gql" +query RelayConnections { + domain(by: { name: "eth" }) { + subdomains(first: 10, order: { by: NAME }) { + totalCount + pageInfo { + hasNextPage + endCursor + } + edges { + node { + canonical { + name { + interpreted + } + } + } + } + } + } +} +``` + +## A complete audit log of ENS Events + +The Omnigraph indexes every onchain Event relevant to ENS and exposes it from the entities each Event relates to: + +- `Domain.events` β€” every Event for a specific Domain +- `Resolver.events` β€” every Event emitted by a specific Resolver +- `Account.events` β€” every Event for which an Account is the HCA-aware `sender` +- `Permissions.events`, `PermissionsUser.events` β€” Permission grant and revocation history + +Each `Event` carries chain, block, transaction, and log metadata, plus an HCA-aware `sender` field distinct from the raw `tx.from` for HCA-mediated transactions. + +```graphql title="example.gql" +query DomainEvents { + domain(by: { name: "vitalik.eth" }) { + events(first: 5) { + totalCount + edges { + node { + timestamp + transactionHash + } + } + } + } +} +``` + +## First-class Permissions + +In ENSv2, many contracts (like `Registry` and `PermissionedResolver`) have **Permissions** indicating who can do what on a given resource within the contract. It's a very flexible system, and the Omnigraph gives developers the power to write the necessary queries to drive UI. + +Permissions are modeled as top-level entities. `Permissions` represents a contract that manages role grants; `PermissionsResource` is an addressable resource within that contract; `PermissionsUser` is a specific user's role bitmap on a specific resource. + +Registries, Resolvers, and ENSv2 Domains all expose their Permissions directly (`Registry.permissions`, `Resolver.permissions`, `ENSv2Domain.permissions`), and an `Account` can be queried for every Permission it's been granted (`Account.permissions`, `Account.registryPermissions`, `Account.resolverPermissions`). Access-aware UIs β€” "which Domains can this address manage?", "who can update this Registry?" β€” become a single query. + +### Permissions a user holds + +Query an `Account` for every Permission it's been granted: + +```graphql title="example.gql" +query PermissionsByUser($address: Address!) { + account(by: { address: $address }) { + permissions { + edges { + node { + resource + roles + } + } + } + } +} +``` + +### Permissions on a contract + +Address a `Permissions` entity by the contract that manages it, then walk its resources and the users granted roles on each: + +```graphql title="example.gql" +query PermissionsByContract($contract: AccountIdInput!) { + permissions(by: { contract: $contract }) { + resources { + edges { + node { + resource + users { + edges { + node { + user { address } + roles + } + } + } + } + } + } + } +} +``` + +### Permissions on a Domain + +Start from a Domain by name and read the roles users hold on that Domain's token (ENSv2 Domains manage Permissions per-token): + +```graphql title="example.gql" +query DomainPermissions { + domain(by: { name: "vitalik.eth" }) { + ... on ENSv2Domain { + permissions { + edges { + node { + user { address } + roles + } + } + } + } + } +} +``` + +## Learn more about ENSv2 + +New to the ENSv2 protocol? These resources from the ENS team explain the architecture and onchain data model that the Omnigraph unifies with ENSv1: + + + + + + + + diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/index.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/index.mdx index 01d4846a50..228c0b6692 100644 --- a/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/index.mdx +++ b/docs/ensnode.io/src/content/docs/docs/integrate/omnigraph/index.mdx @@ -3,151 +3,67 @@ title: ENS Omnigraph API description: The world's first and only API providing unified access to the full state of ENSv1 and ENSv2. --- -import { LinkCard, CardGrid } from "@astrojs/starlight/components"; +import { LinkCard, CardGrid, Aside } from "@astrojs/starlight/components"; The **ENS Omnigraph API** is the world's first and only API providing unified access to the full state of ENSv1 and ENSv2. It's a single, polymorphic GraphQL API over **both ENSv1 and ENSv2** β€” write your query once and get correct, typed results regardless of which protocol version a given Domain lives in. -![ENS Omnigraph diagram](/ens-omnigraph-diagram.png) - -The Omnigraph is delivered by [ENSApi](/docs/services/ensapi) on top of the indexed Unigraph in [ENSDb](/docs/services/ensdb). It follows the [Relay specification](https://relay.dev/graphql/connections.htm), abstracts away the most common ENS-protocol footguns, and exposes enough of the underlying protocol for builders who need to go deep. - -## One unified API over ENSv1 + ENSv2 - -When ENSv2 launches in **Summer 2026**, the two protocol versions coexist β€” and the Omnigraph keeps your app working against both, at the same time, with no code changes. Both ENSv1 and ENSv2 Domains are indexed concurrently and exposed through a unified schema. - -Addressing a Domain by name (`domain(by: { name: "vitalik.eth" })`) returns an ENSv2 Domain if the domain has been optionally upgraded by its owner to ENSv2, and returns an ENSv1 Domain otherwise. Set-returning queries (`domains(where: { name: ... })`) return both the ENSv1 and ENSv2 onchain representations of the same domain, unless explicitly filtered with `version: ENSv1` or `version: ENSv2`. - -## What is a Namegraph? - -A **Namegraph** is the native onchain data model of ENSv2: it represents names not as a flat mapping of namehashes in a Nametable (as in ENSv1), but as a graph of `Registry β†’ Domain β†’ Registry β†’ Domain β†’ …`. This graph may be cyclic, and within the ENSv2 protocol an unbounded number of _disjoint_ (not connected) Namegraphs will exist. Within the unified ENS protocol (v1+v2) there will exist many Namegraphs, at the very least those headed by the ENSv1 Root Registry, Basenames, Lineanames, 3DNS, and the ENSv2 Root Registry. - -## What is the Unigraph? - -The **Unigraph** is the entire collection of these disjoint ENSv2 Namegraphs and multiple ENSv1 Nametables, combined together into a single unified data model using ENS Resolution semantics. Navigating the Unigraph from `"eth"` down to `"vitalik.eth"` and beyond looks identical regardless of whether the underlying entities are ENSv1 or ENSv2. - -The [`unigraph` plugin](/docs/services/ensindexer) in ENSIndexer is what builds this unified model. It's also where multichain coverage lives: Basenames (`.base.eth`), Lineanames (`.linea.eth`), and 3DNS names (`.box`) are all materialized into the same Unigraph as mainnet `.eth`, so a single query covers all indexable names in one shot. - -## Canonicality - -Given that a Domain entity (say, the `sub` in `sub.example.eth`) can be reached by infinitely many aliases (for example, `sub.other.eth`), it becomes important to determine a _canonical_ reference to the Domain β€” this is the **Canonical Name**. Canonicality is also connected to nameability; if an ENSv2 Domain exists on-chain but isn't eventually connected to the Root Registry somehow, **it doesn't have a name!** - -Within the Omnigraph API the complexity of this is reduced, and all Canonical Domains (those with names) are queryable, searchable, and addressable by said name. Domains that are not canonical are _still_ referenceable by `id` (eg. `domain(by: { id: DomainId! })`). - -Canonical Domains have a `Domain.canonical` field hosting the canonicality-derived fields such as `name`, `node`, `depth` (i.e. 2 for `vitalik.eth`), and `path` from the ENS root (i.e. `[Domain("eth"), Domain("vitalik.eth")]`). - -## Stable IDs vs. Namegraph addressing - -Every entity in the Omnigraph has an `id` β€” a nominally-typed, stable reference to a specific on-chain entity (`DomainId`, `RegistryId`, `RegistrationId`, etc.). When you already know the entity you mean, address it by `id`: `domain(by: { id: "..." })` always returns the same on-chain Domain, even if its canonical name changes across time. - -Addressing a Domain by **name** is a different operation. It's **Unigraph forward traversal**: `domain(by: { name: "vitalik.eth" })` walks from the ENSv2 Root Registry β†’ `"eth"` in that Registry β†’ the Registry that `"eth"` points at (the EthRegistry) β†’ `"vitalik"` in that Registry. The Domain returned is whichever on-chain entity (if any) that path currently resolves to. + -**Rule of thumb:** address by `name` when you're answering "which Domain would records come from if resolved right now"; address by `id` when you're answering "what's the latest state of this specific on-chain entity?". - -## Polymorphism via GraphQL interfaces - -`Domain`, `Registry`, and `Registration` are GraphQL **interfaces**, with concrete types implementing each: - -- `Domain` β†’ `ENSv1Domain`, `ENSv2Domain` -- `Registry` β†’ `ENSv1Registry`, `ENSv1VirtualRegistry`, `ENSv2Registry` -- `Registration` β†’ `BaseRegistrarRegistration`, `NameWrapperRegistration`, `ThreeDNSRegistration`, `ENSv2RegistryRegistration`, `ENSv2RegistryReservation` - -Shared fields are available unconditionally on the interface. Protocol- or implementation-specific fields are reached via typed inline fragments β€” `... on ENSv1Domain { rootRegistryOwner }`, `... on ENSv2Domain { tokenId }`, `... on BaseRegistrarRegistration { wrapped { fuses } }`, `... on NameWrapperRegistration { fuses }`. The result is a single query that compiles, type-checks, and returns the right fields for whichever concrete type each record turns out to be. - -## Interpreted Names everywhere - -Every name and label crossing the Omnigraph surface is an **Interpreted Name** (or Interpreted Label). Each label in an Interpreted Name is either a normalized literal label or an Encoded LabelHash (`[abc123…]`) when the literal isn't known or is unnormalized. This eliminates one of the most common ENS UI footguns β€” unnormalized labels, unhealed hashes, and rendering surprises β€” at the schema layer, making UI rendering trivial. See [terminology](/docs/reference/terminology#interpreted-label) for the full definition. - -## Relay-spec connections - -Collection and paginated relationship fields in the schema follow the [Relay-spec Connection](https://relay.dev/graphql/connections.htm) pattern with `edges`, `pageInfo`, and `totalCount`. Cursor-based pagination is idiomatic in urql, Apollo, Relay, and most modern GraphQL clients β€” infinite scroll and stable pagination work out of the box, with no per-endpoint plumbing. - -## A complete audit log of ENS Events +![ENS Omnigraph diagram](/ens-omnigraph-diagram.png) -The Omnigraph indexes every onchain Event relevant to ENS and exposes it from the entities each Event relates to: +The Omnigraph is delivered by [ENSApi](/docs/services/ensapi) on top of the indexed data in [ENSDb](/docs/services/ensdb). It follows the [Relay specification](https://relay.dev/graphql/connections.htm), abstracts away the most common ENS-protocol footguns, and exposes enough of the underlying protocol for builders who need to go deep. -- `Domain.events` β€” every Event for a specific Domain -- `Resolver.events` β€” every Event emitted by a specific Resolver -- `Account.events` β€” every Event for which an Account is the HCA-aware `sender` -- `Permissions.events`, `PermissionsUser.events` β€” Permission grant and revocation history +## One unified API over ENSv1 + ENSv2 -Each `Event` carries chain, block, transaction, and log metadata, plus an HCA-aware `sender` field distinct from the raw `tx.from` for HCA-mediated transactions. +When ENSv2 launches in **Summer 2026**, the two protocol versions coexist β€” and the Omnigraph keeps your app working against both, at the same time, with no code changes. Both ENSv1 and ENSv2 Domains are indexed concurrently and exposed through a unified schema. -## First-class Permissions +Ask for a Domain by name (`domain(by: { name: "vitalik.eth" })`) and you get a typed result whether that name lives in ENSv1 or ENSv2 β€” your code doesn't have to know which at query time. -Permissions are modeled as top-level entities. `Permissions` represents a contract that manages role grants; `PermissionsResource` is an addressable resource within that contract; `PermissionsUser` is a specific user's role bitmap on a specific resource. +## What you get -Registries, Resolvers, and ENSv2 Domains all expose their Permissions directly (`Registry.permissions`, `Resolver.permissions`, `ENSv2Domain.permissions`), and an `Account` can be queried for every Permission it's been granted (`Account.permissions`, `Account.registryPermissions`, `Account.resolverPermissions`). Access-aware UIs β€” "which Domains can this address manage?", "who can update this Registry?" β€” become a single query. +- **Multichain in one query** β€” mainnet `.eth`, Basenames (`.base.eth`), Lineanames (`.linea.eth`), and 3DNS names (`.box`) all in a single unified schema. +- **Typed, no footguns** β€” names and labels are normalized for you, so rendering them in a UI is trivial. +- **The full ENS picture** β€” resolve records, search Domains, list what an address owns, read a complete history of onchain Events, inspect Permissions, and more. +- **Built-in pagination** β€” Relay-spec connections mean infinite scroll and stable pagination work out of the box. -## Example +## Your first query -The polymorphism, Namegraph traversal, Canonical Name, Events, and version-specific fragments β€” all in one query: +Look up a Domain by name and read its owner: -```graphql title="example.graphql" -query EthAndSubdomains { - domains(where: { name: { eq: "eth" } }) { - edges { - node { - __typename - id - canonical { - name { - interpreted - } - depth - } - events(first: 5) { - totalCount - edges { - node { - timestamp - transactionHash - } - } - } - subdomains(first: 10, order: { by: NAME }) { - totalCount - edges { - node { - __typename - id - canonical { - name { - interpreted - } - } - ... on ENSv1Domain { - rootRegistryOwner { - address - } - } - ... on ENSv2Domain { - tokenId - } - } - } - } - ... on ENSv1Domain { - rootRegistryOwner { - address - } - } - ... on ENSv2Domain { - tokenId - } +```graphql title="example.gql" +query GetDomain { + domain(by: { name: "vitalik.eth" }) { + canonical { + name { + interpreted } } + owner { + address + } } } ``` +That's a simple shape to get started. When you're ready to understand the data model underneath, see [Core Concepts](/docs/integrate/omnigraph/concepts). + ## Next steps + - diff --git a/docs/ensnode.io/src/content/docs/docs/reference/terminology.mdx b/docs/ensnode.io/src/content/docs/docs/reference/terminology.mdx index a2021bdd49..793d74dbf4 100644 --- a/docs/ensnode.io/src/content/docs/docs/reference/terminology.mdx +++ b/docs/ensnode.io/src/content/docs/docs/reference/terminology.mdx @@ -132,6 +132,15 @@ An **Interpreted Label** is a **Label** that is either: Apps building on ENSNode should take special note to align their implementation of **ENS Normalize** with the **ENS Normalize** used by the ENSNode they use. The **ENS Normalize** version used by each ENSNode can be referenced in the ENSIndexer Config API. +### Beautified Label + +A **Beautified Label** is a **Label** produced from an **Interpreted Label** for presentation in a UI. It is either: + +- an **Encoded LabelHash**, preserved verbatim from the source **Interpreted Label**, or +- a label produced by passing a normalized label through [ENSIP-15](https://docs.ens.domains/ensip/15) beautification, which is guaranteed to be normalizable back to the original normalized label but is itself NOT necessarily normalized (e.g. `"β™Ύ"` β†’ `"♾️"`). + +Because the beautified form is not guaranteed to be normalized, a **Beautified Label** is NOT an **Interpreted Label** and MUST NOT be used as a lookup key, or anywhere else that expects an **Interpreted Label**. + ### Literal Name A **Literal Name** is a Name exclusively composed of 0 or more **Literal Labels**. @@ -140,6 +149,23 @@ A **Literal Name** is a Name exclusively composed of 0 or more **Literal Labels* An **Interpreted Name** is a Name exclusively composed of 0 or more **Interpreted Labels**. +### Beautified Name + +A **Beautified Name** is a Name produced from an **Interpreted Name** for presentation in a UI, where each label is either: + +- an **Encoded LabelHash**, preserved verbatim from the source **Interpreted Name**, or +- a label produced by passing a normalized label through [ENSIP-15](https://docs.ens.domains/ensip/15) beautification, which is guaranteed to be normalizable back to the original normalized label but is itself NOT necessarily normalized (e.g. `"β™Ύ"` β†’ `"♾️"`). + +Because the beautified form is not guaranteed to be normalized, a **Beautified Name** is NOT an **Interpreted Name** and MUST NOT be used as a navigation target, lookup key, or anywhere else that expects an **Interpreted Name**. + +### Resolvable Name + +A **Resolvable Name** is a Name that can be used as input to ENS Forward Resolution. It contains only literal label segments (normalized or unnormalized), and each label is at most 255 bytes due to the [DNS Encoding](https://docs.ens.domains/resolution/names/#dns-encoding) standard. + +:::danger[Interpreted Name !== Resolvable Name] +Because Forward Resolution does not support **Encoded LabelHashes**, an **Interpreted Name** containing an **Encoded LabelHash** is NOT a **Resolvable Name**: it can be used to traverse the Namegraph, but the records for that name cannot be resolved. +::: + ## Subgraph Indexability & Label/Name Interpretation ### Subgraph-Indexable Labels / Subgraph-Unindexable Labels diff --git a/docs/ensnode.io/src/content/docs/docs/services/ensrainbow/concepts/creating-files.mdx b/docs/ensnode.io/src/content/docs/docs/services/ensrainbow/concepts/creating-files.mdx index 039aad0e33..3cc82b8eda 100644 --- a/docs/ensnode.io/src/content/docs/docs/services/ensrainbow/concepts/creating-files.mdx +++ b/docs/ensnode.io/src/content/docs/docs/services/ensrainbow/concepts/creating-files.mdx @@ -446,8 +446,8 @@ Create documentation for your custom label set including: ````markdown ## Custom Label Set: my-dataset -**Label Set ID**: `my-dataset` -**Current Version**: `0` +**Label Set ID**: `my-dataset` +**Current Version**: `0` **Description**: Custom ENS labels from [source description] ### Download