Skip to content

Conversation

@shrugs
Copy link
Collaborator

@shrugs shrugs commented Jan 29, 2026

closes #1565

Reviewer Focus (Read This First)

  • recursive CTE correctness in find-domains.ts - the v1/v2 path traversal logic is the core of this PR
    • this was written by claude but seems functionally correct to me
  • isInterpetedLabel bug fix - previously fell through to isNormalizedLabel for encoded labelhashes

Problem & Motivation

  • materializing canonical names at index-time is impractical due to ponder cache semantics and retroactive label healing
  • need to support "autocomplete" / "search my domains" use case
  • query-time calculation via recursive CTEs is more correct and avoids indexing complexity

What Changed (Concrete)

  1. new findDomains() function with recursive CTEs for v1/v2 domain path traversal
  2. new registryCanonicalDomain schema table for ENSv2 canonical parent tracking
  3. split getCanonicalPath into getV1CanonicalPath and getV2CanonicalPath
  4. new indexes: v1Domain.byLabelHash, v2Domain.byLabelHash, label.byValue
  5. new SDK functions: parsePartialInterpretedName, constructSubInterpretedName, ensureInterpretedLabel, interpretedLabelsToLabelHashPath
  6. fix: isInterpetedLabel now returns early for valid encoded labelhashes
  7. new Query.domains endpoint with where: { name, owner } filter
  8. MAX_DEPTH (16) validation to prevent unbounded CTE recursion

Design & Planning

  • query-time approach chosen over index-time materialization due to ponder constraints
  • inverted CTE traversal (leaf→root) for efficiency vs traversing down from all domains
  • registryCanonicalDomain as interim solution until ENSv2 canonical names are implemented on-chain

Self-Review

  • changed innerJoin to leftJoin on schema.label so owner-only queries don't exclude unlabeled domains

  • added MAX_DEPTH validation after initial implementation

  • Bugs caught: isInterpetedLabel not returning early for encoded labelhashes

  • Logic simplified: n/a

  • Naming / terminology improved: renamed interpretedNameToLabelHashPathinterpretedLabelsToLabelHashPath

  • Dead or unnecessary code removed: n/a


Cross-Codebase Alignment

  • checked get-canonical-path.ts for MAX_DEPTH constant alignment
  • reviewed existing index definitions in schema before adding new ones

Downstream & Consumer Impact

  • new Query.domains(where: { name, owner }) endpoint available (dev-only for now)
  • existing APIs unchanged
  • new schema tables/indexes will require migration
  • Naming decisions worth calling out: concrete vs partial terminology for parsed name segments

Testing Evidence

  • unit tests for parsePartialInterpretedName (30 cases)
  • unit tests for constructSubInterpretedName (14 cases)
  • typecheck and lint pass
  • manual testing against devnet
  • Known gaps: no integration tests for recursive CTEs, no tests for findDomains itself

Scope Reductions

  • integration tests for findDomains

  • LIKE operator input escaping (marked with TODO)

  • production-ready Query.domains endpoint (currently dev-only)

  • Follow-ups: integration tests, input escaping, promote endpoint to production

  • Why they were deferred: focused on core functionality first


Risk Analysis

  • recursive CTE could be slow for deep hierarchies (mitigated by MAX_DEPTH=16)
  • new indexes increase write overhead during indexing
  • registryCanonicalDomain is an interim solution that will need migration

Pre-Review Checklist (Blocking)

  • I reviewed every line of this diff and understand it end-to-end
  • I'm prepared to defend this PR line-by-line in review
  • I'm comfortable being the on-call owner for this change
  • Relevant changesets are included (or explicitly not required)

Copilot AI review requested due to automatic review settings January 29, 2026 03:36
@changeset-bot
Copy link

changeset-bot bot commented Jan 29, 2026

🦋 Changeset detected

Latest commit: 86f23fe

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
ensapi Major
ensindexer Major
ensadmin Major
ensrainbow Major
fallback-ensapi Major
@ensnode/datasources Major
@ensnode/ensrainbow-sdk Major
@ensnode/ponder-metadata Major
@ensnode/ensnode-schema Major
@ensnode/ensnode-react Major
@ensnode/ponder-subgraph Major
@ensnode/ensnode-sdk Major
@ensnode/shared-configs Major
@docs/ensnode Major
@docs/ensrainbow Major
@docs/mintlify Major
@namehash/ens-referrals Major
@namehash/namehash-ui Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Contributor

vercel bot commented Jan 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Feb 2, 2026 7:20am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
ensnode.io Skipped Skipped Feb 2, 2026 7:20am
ensrainbow.io Skipped Skipped Feb 2, 2026 7:20am

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Caution

Review failed

The head commit changed during the review from c8af3cc to 86f23fe.

📝 Walkthrough

Walkthrough

Adds partial-name domain search across ENSv1/v2, separate v1/v2 canonical-path computation and per-request canonical-path dataloaders; exposes Domain.name/path and domains query filters; introduces interpretation utilities for partial names, schema/indexer/index changes, and registry→canonical-domain mappings.

Changes

Cohort / File(s) Summary
Domain search & canonical path
apps/ensapi/src/graphql-api/lib/find-domains.ts, apps/ensapi/src/graphql-api/lib/get-canonical-path.ts
New findDomains({name, owner}) that unionizes v1/v2 recursive-CTE traversals; splits canonical-path logic into getV1CanonicalPath and getV2CanonicalPath; renames ENSv2 root constant to ENSv2_ROOT_REGISTRY_ID.
GraphQL schema & resolvers
apps/ensapi/src/graphql-api/schema/domain.ts, apps/ensapi/src/graphql-api/schema/query.ts, apps/ensapi/src/graphql-api/schema/account.ts, apps/ensapi/src/graphql-api/schema/permissions.ts, apps/ensapi/src/graphql-api/schema/registration.ts, apps/ensapi/src/graphql-api/schema/registry.ts, apps/ensapi/src/graphql-api/schema/resolver.ts
Adds Domain.name and Domain.path resolvers, DomainsWhereInput/AccountDomainsWhereInput, dev-only domains query wired to findDomains; Account.domains accepts where; refactors many where predicates to inline conditional before/after expressions; bridged resolver now uses config-derived namespace.
Interpretation utilities & FQDN pipeline
packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts, apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts, packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.test.ts
Renames interpretedNameToLabelHashPathinterpretedLabelsToLabelHashPath (now takes InterpretedLabel[]); adds parsePartialInterpretedName, ensureInterpretedLabel, constructSubInterpretedName; updates FQDN pipeline and expands tests for partial/encoded-label cases.
Database schema & indexes
packages/ensnode-schema/src/schemas/ensv2.schema.ts
Adds registry_canonical_domains table; adds byLabelHash indexes to v1Domain/v2Domain; adds byValue index on label.
ENSv2 indexer: bridging awareness
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
Adds bridging detection for L2 ETH registries; computes bridged domain IDs and upserts/deletes registryCanonicalDomain entries (last-write-wins heuristic) on NameRegistered and SubregistryUpdated.
Protocol-acceleration/resolver lookup
apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts
Makes inclusion of RegistryOld branch conditional on registry type instead of unconditional inclusion (changes resolver lookup predicates).
Context, Yoga, and loaders
apps/ensapi/src/graphql-api/context.ts, apps/ensapi/src/graphql-api/yoga.ts, apps/ensapi/src/graphql-api/builder.ts
Adds per-request DataLoaders for v1/v2 canonical-paths and a context() factory; wires imported context into Yoga; adjusts SchemaBuilder Context generic to ReturnType<typeof context>.
Tests/config/deps
packages/datasources/src/ens-test-env.ts, pnpm-workspace.yaml
Updates ENSv1 test contract addresses and bumps ponder from 0.16.1→0.16.2.
Misc schema resolvers
various schema files listed above
Refactors multiple where-clause constructions to use inline conditional expressions for optional before/after pagination predicates (semantic parity).

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Query as GraphQL Query
    participant FindDomains as findDomains()
    participant DB as Database
    participant Loader as Dataloader

    Client->>Query: domains(where: {name?, owner?})
    Query->>FindDomains: findDomains({name, owner})
    FindDomains->>FindDomains: parsePartialInterpretedName → interpretedLabelsToLabelHashPath
    FindDomains->>DB: execute v1 recursive CTE (labelHashPath)
    DB-->>FindDomains: v1 domain IDs
    FindDomains->>DB: execute v2 recursive CTE (registryCanonicalDomain)
    DB-->>FindDomains: v2 domain IDs
    FindDomains->>FindDomains: unionAll(v1, v2) + filters + pagination
    FindDomains-->>Query: paginated Domain IDs
    Query->>Loader: load DomainInterfaceRef by IDs
    Loader->>DB: fetch domain records
    DB-->>Loader: domain data
    Loader-->>Query: DomainInterfaceRef[]
    Query-->>Client: domains connection
Loading
sequenceDiagram
    participant Resolver as Domain.name Resolver
    participant CanonicalPath as getV1CanonicalPath / getV2CanonicalPath
    participant DB as Database
    participant Loader as Dataloader
    participant Interpreter as interpretedLabelsToInterpretedName

    Resolver->>CanonicalPath: request canonical path for domainId
    CanonicalPath->>DB: recursive CTE (v1 via parent_id, v2 via registryCanonicalDomain)
    DB-->>CanonicalPath: ordered domain IDs (leaf→root)
    CanonicalPath-->>Resolver: CanonicalPath[]
    Resolver->>Loader: load domains for path IDs
    Loader->>DB: fetch domain rows
    DB-->>Loader: domain rows
    Loader-->>Resolver: Domain objects
    Resolver->>Interpreter: interpretedLabelsToInterpretedName(labels)
    Interpreter-->>Resolver: Name
    Resolver-->>Client: domain.name
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

ensnode-sdk

Poem

🐰 I hop through labels, hashes, and root,
Stitching v1 and v2 paths, one careful route,
I parse a partial name, chase canonical light,
Union the tracks, and bind day to night,
A rabbit cheers: canonical names take root!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat/canonical name heuristic' is directly related to the PR's main objective of implementing canonical name materialization using a query-time heuristic approach.
Description check ✅ Passed The PR description comprehensively covers the problem, changes, design decisions, testing, and known gaps, following the repository template with detailed sections on motivation, concrete changes, and risk analysis.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #1565: canonical name materialization using last-write-wins heuristic, surfaced via Query.domains and Account.domains search endpoints for autocomplete/search use cases.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the canonical name heuristic feature. Supporting changes (schema updates, SDK utilities, context management, resolver refactoring) are all necessary to enable the core functionality and query-time resolution.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/canonical-name-heuristic

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a canonical name heuristic for ENS domains to support domain search and autocomplete functionality. Since ENSv2 canonical names are not yet fully implemented by the ENS team, this PR introduces a "last-write-wins" heuristic for tracking canonical domain references.

Changes:

  • Updates ponder dependency from 0.16.1 to 0.16.2
  • Adds registryCanonicalDomain schema table to track registry-to-canonical-domain mappings using a heuristic approach
  • Implements separate canonical path resolution for ENSv1 (tree-based) and ENSv2 (graph-based with canonical domain tracking)
  • Adds Domain.name and Domain.path GraphQL fields to expose canonical names
  • Introduces findDomains function for domain search/filtering with partial name support (though implementation is incomplete)
  • Refactors SDK functions for better separation of concerns (e.g., interpretedLabelsToLabelHashPath)

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
pnpm-workspace.yaml Updates ponder catalog version to 0.16.2
pnpm-lock.yaml Updates ponder lock entries to version 0.16.2 across all packages and snapshots
packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts Refactors interpretedLabelsToLabelHashPath to accept labels instead of name; adds utility functions for constructing interpreted names, ensuring interpreted labels, and parsing partial interpreted names
packages/ensnode-schema/src/schemas/ensv2.schema.ts Adds registryCanonicalDomain table to track temporary canonical domain heuristic until ENS team implements proper canonical names
packages/datasources/src/ens-test-env.ts Updates test environment contract addresses for ENSv1RegistryOld and ENSv1Registry
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts Implements last-write-wins heuristic for canonical domains in SubregistryUpdated handler; removes debug console.log
apps/ensapi/src/graphql-api/schema/query.ts Adds development-only Query.domains connection for testing domain search functionality
apps/ensapi/src/graphql-api/schema/domain.ts Replaces commented-out canonical name fields with implemented Domain.name and Domain.path fields that use canonical path resolution
apps/ensapi/src/graphql-api/schema/account.ts Refactors Account.domains to use new findDomains helper instead of inline query logic; removes unused unionAll import
apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts Updates function call from interpretedNameToLabelHashPath to interpretedLabelsToLabelHashPath with proper parameter conversion
apps/ensapi/src/graphql-api/lib/get-canonical-path.ts Splits canonical path logic into getV1CanonicalPath (tree-based) and getV2CanonicalPath (uses registry_canonical_domains table); adds ROOT_NODE import
apps/ensapi/src/graphql-api/lib/find-domains.ts Adds new domain filtering/search function supporting name-based and owner-based queries (though name filtering for v2 and partial label matching remain unimplemented)
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts (1)

192-206: Clean up stale canonical mappings when subregistries change or are removed.

If a subregistry is cleared or replaced, the previous registryCanonicalDomain row remains, so canonical-path resolution can keep treating the old registry as canonical. Capture the previous subregistryId and delete its mapping before updating/clearing.

🛠️ Suggested fix (remove old mapping before update)
       const registryAccountId = getThisAccountId(context, event);
       const canonicalId = getCanonicalId(tokenId);
       const domainId = makeENSv2DomainId(registryAccountId, canonicalId);
+      const existing = await context.db.find(schema.v2Domain, { id: domainId });
+      const previousSubregistryId = existing?.subregistryId ?? null;

       // update domain's subregistry
       if (subregistry === null) {
+        if (previousSubregistryId) {
+          await context.db.delete(schema.registryCanonicalDomain, {
+            registryId: previousSubregistryId,
+          });
+        }
         await context.db.update(schema.v2Domain, { id: domainId }).set({ subregistryId: null });
       } else {
         const subregistryAccountId: AccountId = { chainId: context.chain.id, address: subregistry };
         const subregistryId = makeRegistryId(subregistryAccountId);

+        if (previousSubregistryId && previousSubregistryId !== subregistryId) {
+          await context.db.delete(schema.registryCanonicalDomain, {
+            registryId: previousSubregistryId,
+          });
+        }
         await context.db.update(schema.v2Domain, { id: domainId }).set({ subregistryId });

         // TODO(canonical-names): this implements last-write-wins heuristic for a Registry's canonical name,
         // replace with real logic once ENS Team implements Canonical Names
         await context.db
packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts (1)

108-128: Update TypeScript target to ES2023 or use a compatible alternative.

The code uses Array.prototype.toReversed() (ES2023), but tsconfig.lib.json declares target: "ES2022". While the project's minimum Node version (24.13.0) natively supports ES2023, this creates a configuration inconsistency. Either update the TypeScript target to ES2023 or replace toReversed() with .reverse() to maintain ES2022 compatibility.

🔧 Alternative for ES2022 compatibility
-    .toReversed();
+    .reverse();
apps/ensapi/src/graphql-api/lib/get-canonical-path.ts (1)

61-63: Update invariant labels to the new helper names.

The thrown error still references getCanonicalPath, which no longer exists, making logs misleading.

🛠️ Proposed fix
-  if (rows.length === 0) {
-    throw new Error(`Invariant(getCanonicalPath): DomainId '${domainId}' did not exist.`);
-  }
+  if (rows.length === 0) {
+    throw new Error(`Invariant(getV2CanonicalPath): DomainId '${domainId}' did not exist.`);
+  }
@@
-  if (rows.length === 0) {
-    throw new Error(`Invariant(getCanonicalPath): DomainId '${domainId}' did not exist.`);
-  }
+  if (rows.length === 0) {
+    throw new Error(`Invariant(getV1CanonicalPath): DomainId '${domainId}' did not exist.`);
+  }

Also applies to: 110-112

🤖 Fix all issues with AI agents
In `@apps/ensapi/src/graphql-api/lib/find-domains.ts`:
- Around line 164-168: findV2Domains currently ignores the DomainFilter.name
(and canonical path), causing unbounded v2 results; update the function to apply
name/canonicalPath filtering (or return no rows when name is provided until
implemented). Specifically, in findV2Domains (which queries schema.v2Domain),
include an additional where clause that filters by eq(schema.v2Domain.name,
name) or eq(schema.v2Domain.canonicalPath, name) when DomainFilter.name is
present, combining with the existing owner filter (e.g., using and(...) or
building an array of conditions) so the query is constrained; alternatively, if
you prefer to fail closed until canonical-path logic exists, have the function
return an empty selection when name is provided.
- Around line 108-160: The function findV1DomainsByName must handle the case
where parsePartialInterpretedName(name) yields an empty concrete array; add an
early-return when concrete.length === 0 that returns all v1 domains (so
owner-only queries still return domains) instead of building the CTE. In
practice, inside findV1DomainsByName, check if concrete.length === 0 and
immediately return a simple query against schema.v1Domain (similar to the
commented-out snippet that selects schema.v1Domain.id), before computing
labelHashPath/rawLabelHashPathArray/pathLength or constructing the recursive
CTE.

In `@apps/ensapi/src/graphql-api/schema/domain.ts`:
- Around line 124-129: The invariant error message inside the canonicalPath
mapping is still labeled "Domain.canonicalName"; update that message to
reference "Domain.name" instead to match the current model. Locate the mapping
over canonicalPath where domains.find((d) => d.id === domainId) is used and
change the thrown Error text (`Invariant(Domain.canonicalName): ...`) to
`Invariant(Domain.name): ...`, preserving the existing Path and DomainId
details.

Copilot AI review requested due to automatic review settings January 30, 2026 02:28
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 30, 2026 02:28 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 30, 2026 02:28 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io January 30, 2026 02:28 Inactive
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 11 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@apps/ensapi/src/graphql-api/lib/find-domains.ts`:
- Around line 121-130: The Promise.all block that runs
db.select().from(v1DomainsByName) and db.select().from(v2DomainsByName) (and
logs v1Domains.toSQL()/v2Domains.toSQL() and full results) should be removed or
gated behind a development/debug flag and protected with error handling; update
the code around the Promise.all(...) block so it only executes when a debug/dev
mode (e.g., process.env.NODE_ENV !== 'production' or a dedicated isDebug flag)
is true, await the queries inside a try/catch (or add .catch) to avoid unhandled
rejections, and avoid logging full result sets—log only the SQL
(v1Domains.toSQL().sql, v2Domains.toSQL().sql) or a small summary instead.
- Around line 93-119: The owner-only queries exclude domains without label rows
because schema.label is inner-joined unconditionally in the v1Domains and
v2Domains query builders; change the join on schema.label to a leftJoin (or
perform the join only when partial is truthy) so that owner-only searches still
return domains with no label row, while keeping the existing where clause using
partial and like(schema.label.value, `${partial}%`) when partial is provided;
update both v1Domains and v2Domains to reference schema.label via leftJoin (or
wrap the join in a conditional based on the partial variable) so unlabeled
domains are not filtered out.

In `@apps/ensapi/src/graphql-api/schema/query.ts`:
- Around line 38-45: The GraphQL input DomainsWhereInput currently uses isOneOf
which enforces exactly one of name or owner, conflicting with the resolver
findDomains that accepts both; update the schema by removing isOneOf from the
builder.inputType call (i.e., adjust the DomainsWhereInput definition so both
name and owner can be provided), or if you need "at least one" semantics add
runtime validation via Pothos Validation plugin in the same DomainsWhereInput to
enforce at-least-one instead of using isOneOf.

Copilot AI review requested due to automatic review settings February 1, 2026 01:16
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 1, 2026 01:16 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 1, 2026 01:16 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 1, 2026 01:16 Inactive
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @.changeset/eight-beans-behave.md:
- Line 8: Fix the typo in the example for Account.domains by adding the missing
closing quotation mark to the name value; update `Account.domains(where?: {
name: "example.et })` to have a matching quote for the `name` string so it reads
`Account.domains(where?: { name: "example.et" })`, ensuring the
`Account.domains` example is valid.
- Line 5: Replace the misspelled word "experiemental" in the changeset sentence
that mentions the ENSv2 API (the phrase starting "The experiemental ENSv2
API...") with the correct spelling "experimental" so the line reads "The
experimental ENSv2 API now supports the following Domain filters, namely
matching indexed Domains by name prefix."

In `@apps/ensapi/src/graphql-api/context.ts`:
- Around line 8-16: The two DataLoader factories createV1CanonicalPathLoader and
createV2CanonicalPathLoader currently call getV1CanonicalPath/getV2CanonicalPath
per id, causing N separate recursive CTE queries; change them to call a new
batched function (e.g., batchGetV1CanonicalPaths and batchGetV2CanonicalPaths)
that accepts the full domainIds array and performs a single SQL query that
computes canonical paths for all requested ids in one go (using a recursive CTE
with an IN (...) filter, join to a temp table, or a VALUES list), then return
results in the same order as input, mapping missing rows to null so DataLoader
receives an array aligned with domainIds. Ensure the new batch functions are
used inside DataLoader constructors and preserve existing types (CanonicalPath |
null) and error-handling.
- Around line 8-16: The batch loaders createV1CanonicalPathLoader and
createV2CanonicalPathLoader currently use Promise.all over
getV1CanonicalPath/getV2CanonicalPath so a single thrown error rejects the whole
batch; change each batch function to map domainIds to individual async calls
wrapped in try/catch and return either the CanonicalPath value or an Error
instance for that key (preserving input order) so DataLoader receives per-key
results/errors; ensure you return an array of (CanonicalPath | Error | null)
matching domainIds for both createV1CanonicalPathLoader and
createV2CanonicalPathLoader.

In `@apps/ensapi/src/graphql-api/schema/domain.ts`:
- Around line 110-133: The Domain.name resolver currently assumes every loaded
domain has a label and will throw when found.label or found.label.value is
missing; change the labels mapping in the resolve function (the block using
isENSv1Domain, context.loaders.v1CanonicalPath/v2CanonicalPath,
DomainInterfaceRef.getDataloader, and interpretedLabelsToInterpretedName) to
guard against missing labels by checking found and found.label.value for each
domainId and returning null (or a chosen fallback) from the resolver immediately
if any label is absent instead of throwing an Error.

Copy link
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

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

@shrugs Looks awesome 🚀 Nice work. Please merge when ready 👍

Copilot AI review requested due to automatic review settings February 2, 2026 07:17
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 2, 2026 07:17 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 2, 2026 07:17 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 2, 2026 07:19 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 2, 2026 07:19 Inactive
@shrugs shrugs merged commit 6e98fb6 into main Feb 2, 2026
15 of 16 checks passed
@shrugs shrugs deleted the feat/canonical-name-heuristic branch February 2, 2026 07:23
@github-actions github-actions bot mentioned this pull request Feb 2, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 7 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +320 to +326
export const DomainsWhereInput = builder.inputType("DomainsWhereInput", {
description: "Filter for domains query. Requires one of name or owner.",
fields: (t) => ({
name: t.string(),
owner: t.field({ type: "Address" }),
}),
});
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The DomainsWhereInput allows both name and owner to be optional in the GraphQL schema, but findDomains requires at least one to be provided and throws at runtime if neither is given. This validation would be better expressed at the GraphQL schema level. Consider making the input type require at least one field, either through a union type approach or by adjusting the schema design, so clients get schema-level validation instead of runtime errors.

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +137
// TODO: determine if it's necessary to additionally escape user input for LIKE operator
// Note: if label is NULL (unlabeled domain), LIKE returns NULL and filters out the row.
// This is intentional - we can't match partial text against unknown labels.
partial ? like(schema.label.value, `${partial}%`) : undefined,
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The LIKE operator is being used with user input without escaping special characters like %, _, or . PostgreSQL LIKE treats % as a wildcard for any characters and _ as a wildcard for a single character. User input containing these characters could lead to unintended matches or performance issues. Consider sanitizing the partial input by escaping these special characters, or use a different matching strategy.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +51
address: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707",
startBlock: 0,
},
ENSv1Registry: {
abi: root_Registry, // Registry was redeployed, same abi
address: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707",
address: "0x0165878a594ca255338adfa4d48449f69242eb8f",
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The test environment addresses have been changed, but there's no explanation in the PR description or commit for why these specific addresses changed. The ENSv1RegistryOld address changed from "0x0dcd1bf9a1b36ce34237eeafef220932846bcd82" to "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707", and ENSv1Registry address changed from "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707" to "0x0165878A594ca255338adfa4d48449f69242Eb8F". These changes swap the addresses between the old and new registry contracts, which could indicate a redeployment of the test environment. Consider adding a comment or updating the PR description to explain this change.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +20
new DataLoader<ENSv1DomainId, CanonicalPath | null>(async (domainIds) =>
Promise.all(domainIds.map((id) => getV1CanonicalPath(id).catch(errorAsValue))),
);

const createV2CanonicalPathLoader = () =>
new DataLoader<ENSv2DomainId, CanonicalPath | null>(async (domainIds) =>
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The DataLoader return type is declared as CanonicalPath | null, but the batch function uses .catch(errorAsValue) which converts errors to Error instances. This means the actual return type is Error | CanonicalPath | null. DataLoaders should be typed to match their actual return values. The type should be DataLoader<ENSv1DomainId, CanonicalPath | null | Error> to accurately reflect what the loader can return.

Suggested change
new DataLoader<ENSv1DomainId, CanonicalPath | null>(async (domainIds) =>
Promise.all(domainIds.map((id) => getV1CanonicalPath(id).catch(errorAsValue))),
);
const createV2CanonicalPathLoader = () =>
new DataLoader<ENSv2DomainId, CanonicalPath | null>(async (domainIds) =>
new DataLoader<ENSv1DomainId, CanonicalPath | null | Error>(async (domainIds) =>
Promise.all(domainIds.map((id) => getV1CanonicalPath(id).catch(errorAsValue))),
);
const createV2CanonicalPathLoader = () =>
new DataLoader<ENSv2DomainId, CanonicalPath | null | Error>(async (domainIds) =>

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +20
new DataLoader<ENSv1DomainId, CanonicalPath | null>(async (domainIds) =>
Promise.all(domainIds.map((id) => getV1CanonicalPath(id).catch(errorAsValue))),
);

const createV2CanonicalPathLoader = () =>
new DataLoader<ENSv2DomainId, CanonicalPath | null>(async (domainIds) =>
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The DataLoader return type is declared as CanonicalPath | null, but the batch function uses .catch(errorAsValue) which converts errors to Error instances. This means the actual return type is Error | CanonicalPath | null. DataLoaders should be typed to match their actual return values. The type should be DataLoader<ENSv2DomainId, CanonicalPath | null | Error> to accurately reflect what the loader can return.

Suggested change
new DataLoader<ENSv1DomainId, CanonicalPath | null>(async (domainIds) =>
Promise.all(domainIds.map((id) => getV1CanonicalPath(id).catch(errorAsValue))),
);
const createV2CanonicalPathLoader = () =>
new DataLoader<ENSv2DomainId, CanonicalPath | null>(async (domainIds) =>
new DataLoader<ENSv1DomainId, CanonicalPath | null | Error>(async (domainIds) =>
Promise.all(domainIds.map((id) => getV1CanonicalPath(id).catch(errorAsValue))),
);
const createV2CanonicalPathLoader = () =>
new DataLoader<ENSv2DomainId, CanonicalPath | null | Error>(async (domainIds) =>

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +8
- `Query.domains(where: { name?: "example.et", owner?: "0xdead...beef" })`
- `Account.domains(where?: { name: "example.et" })`
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The changeset examples show "example.et" which appears to be a typo or an incomplete example. Since this feature supports partial name matching, this might be intentional to demonstrate partial matching, but it could be confusing. Consider clarifying whether this is showing a partial match example or using a complete example like "example.eth".

Suggested change
- `Query.domains(where: { name?: "example.et", owner?: "0xdead...beef" })`
- `Account.domains(where?: { name: "example.et" })`
- `Query.domains(where: { name?: "example.eth", owner?: "0xdead...beef" })`
- `Account.domains(where?: { name: "example.eth" })`

Copilot uses AI. Check for mistakes.
Comment on lines +274 to +277
FROM ${schema.v2Domain} d
JOIN ${schema.registryCanonicalDomain} rcd
ON rcd.registry_id = d.registry_id
WHERE d.label_hash = (${rawLabelHashPathArray})[${pathLength}]
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The recursive CTE in v2DomainsByLabelHashPath uses INNER JOINs on registryCanonicalDomain in both the base case and recursive step. This means domains without an entry in registryCanonicalDomain will be excluded from results. While the comment explains this is intentional for canonical path matching, consider the operational implications: if registryCanonicalDomain entries are missing or delayed during indexing, valid domains might not appear in search results until those entries are created. This could affect user experience during initial indexing or if there are gaps in the data.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ENSv2 Canonical Name Heuristic

3 participants