Skip to content

ENSv2: Index ENSv1 Manager and Surface via API#1733

Merged
shrugs merged 4 commits intomainfrom
feat/ensv1-manager
Mar 9, 2026
Merged

ENSv2: Index ENSv1 Manager and Surface via API#1733
shrugs merged 4 commits intomainfrom
feat/ensv1-manager

Conversation

@shrugs
Copy link
Collaborator

@shrugs shrugs commented Mar 7, 2026

closes #1731

Reviewer Focus (Read This First)

  • the change to handleTransfer in ENSv1Registry.ts — previously deleted domains on transfer to zero address, which was not technically correct, since they do still exist

Problem & Motivation

  • ensv1 domains have a concept of a "rootRegistryOwner" (the owner() in the ENSv1 Registry), which was not being tracked as a distinct field
  • the previous handleTransfer implementation deleted domains when transferred to zero address, which is incorrect — transferring registry ownership to zero doesn't mean the domain ceases to exist

What Changed (Concrete)

  1. added rootRegistryOwnerId column to v1Domain schema (ensv2.schema.ts)
  2. added rootRegistryOwner drizzle relation on v1Domain pointing to Account
  3. handleNewOwner now sets rootRegistryOwnerId after upserting the domain
  4. handleTransfer no longer deletes domains on zero-address transfer — instead updates rootRegistryOwnerId (with interpretAddress to normalize zero to null) and always materializes the effective owner
  5. exposed rootRegistryOwner field on ENSv1DomainRef in the graphql api (domain.ts)
  6. added rootRegistryOwner inline fragment to example query in ensnode-sdk
  7. removed unused _owner / owner reassignment pattern in handleTransfer

Design & Planning

  • straightforward addition of a tracked field that was previously implicit in registry events

  • no upfront design doc needed — small, self-contained change

  • Planning artifacts: none

  • Reviewed / approved by: n/a


Self-Review

  • Bugs caught: n/a
  • Logic simplified: collapsed the delete-or-update branch in handleTransfer into a single update + materialize path
  • Naming / terminology improved: n/a
  • Dead or unnecessary code removed: removed the unused _owner rename pattern; removed the stub (second commit)

Cross-Codebase Alignment

  • Search terms used: rootRegistryOwnerId, v1Domain, handleTransfer, handleNewOwner
  • Reviewed but unchanged: materializeENSv1DomainEffectiveOwner — confirmed it handles null correctly
  • Deferred alignment: n/a

Downstream & Consumer Impact

  • Public APIs affected: ENSv1Domain now exposes a nullable rootRegistryOwner field via graphql
  • Docs updated: example query updated in ensnode-sdk
  • Naming decisions worth calling out: rootRegistryOwner aligns with ENS terminology — the registry owner() is the "rootRegistryOwner" of a domain, distinct from the effective owner

Testing Evidence

  • Testing performed: typecheck, lint
  • Known gaps: no unit tests for the new rootRegistryOwner indexing logic
  • What reviewers have to reason about manually: correctness of no longer deleting domains on zero-address transfer

Scope Reductions

  • Follow-ups: none identified
  • Why they were deferred: n/a

Risk Analysis

  • Risk areas: schema migration adds a new nullable column — should be non-breaking. removing domain deletion on zero-address transfer changes indexing behavior.
  • Mitigations or rollback options: revert the PR; re-index
  • Named owner if this causes problems: @shrugs

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)

@shrugs shrugs requested a review from a team as a code owner March 7, 2026 00:39
Copilot AI review requested due to automatic review settings March 7, 2026 00:39
@changeset-bot
Copy link

changeset-bot bot commented Mar 7, 2026

🦋 Changeset detected

Latest commit: b3d2bfe

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/ensnode-schema Major
@ensnode/ensnode-react Major
@ensnode/ensnode-sdk Major
@ensnode/ponder-sdk Major
@ensnode/ponder-subgraph 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 Mar 7, 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 Mar 9, 2026 4:44pm
ensnode.io Ready Ready Preview, Comment Mar 9, 2026 4:44pm
ensrainbow.io Ready Ready Preview, Comment Mar 9, 2026 4:44pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 7, 2026

📝 Walkthrough

Walkthrough

Adds a new nullable field rootRegistryOwner to ENSv1Domain (backed by rootRegistryOwnerId), wires the field into the schema and relations, updates indexer handlers to maintain rootRegistryOwnerId during NewOwner/Transfer events, and surfaces the field in example GraphQL queries and changelog.

Changes

Cohort / File(s) Summary
GraphQL schema
apps/ensapi/src/graphql-api/schema/domain.ts
Added rootRegistryOwner field on ENSv1Domain that resolves from parent.rootRegistryOwnerId (nullable AccountRef).
Indexer handlers
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts
Updater/upsert now sets rootRegistryOwnerId; handleNewOwner and handleTransfer always update rootRegistryOwnerId then re-materialize domain owner (removed prior null-conditional branches).
Database schema / relations
packages/ensnode-schema/src/schemas/ensv2.schema.ts
Added rootRegistryOwnerId field to v1Domain and a rootRegistryOwner relation linking v1Domain.rootRegistryOwnerIdaccount.id (zeroAddress treated as null).
SDK examples
packages/ensnode-sdk/src/graphql-api/example-queries.ts
Updated DomainByName example to include ... on ENSv1Domain { rootRegistryOwner { address } }.
Changelog
.changeset/strong-baboons-help.md
Added changeset entry announcing the new GraphQL field ENSv1Domain.rootRegistryOwner.

Sequence Diagram(s)

sequenceDiagram
    participant Event as ENS Registry Event
    participant Indexer as ENS Indexer (ENSv1Registry handler)
    participant DB as Database (v1Domain.row)
    participant Materializer as Domain Materializer
    participant API as GraphQL API

    Event->>Indexer: NewOwner / Transfer (node, owner)
    Indexer->>DB: upsert v1Domain (id, parentId, labelHash)
    Indexer->>DB: update rootRegistryOwnerId = owner (zeroAddress -> null)
    Indexer->>Materializer: materialize domain owner
    Materializer->>DB: update owner-related fields
    API->>DB: query ENSv1Domain { rootRegistryOwner }
    DB-->>API: return rootRegistryOwnerId -> resolve Account
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nudged a root owner into view,
From events to rows and through the queue,
A field now hums where nodes reside,
Indexed, exposed, with code to guide,
Hooray — the registry's keeper's in sight! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'ENSv2: Index ENSv1 Manager and Surface via API' clearly summarizes the main changes: indexing the ENSv1 Registry owner field and exposing it through the GraphQL API.
Description check ✅ Passed The PR description comprehensively covers all required template sections: Summary, Why (linked issue), Testing, Notes for Reviewer, and Pre-Review Checklist. It provides extensive context, design rationale, and detailed change explanations.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #1731: adds rootRegistryOwnerId field to v1Domain schema, creates drizzle relation to Account, updates handlers to track and normalize the field, and exposes it via GraphQL API.
Out of Scope Changes check ✅ Passed All changes are directly scoped to indexing and surfacing the ENSv1 Registry owner field. The removal of unnecessary code patterns and the behavioral fix to handleTransfer are both justified within scope.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ensv1-manager

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

Adds first-class tracking of the ENSv1 Registry “manager” (the Registry owner() for a node) to the ENSv2 data model and exposes it via the GraphQL API, while correcting indexing behavior to not delete domains when Registry ownership is transferred to the zero address.

Changes:

  • Added nullable managerId to v1Domain plus a Drizzle manager relation to Account.
  • Updated ENSv1 Registry event handlers to persist managerId and to stop deleting v1Domain rows on zero-address transfers.
  • Exposed manager on ENSv1Domain in the GraphQL schema and updated the SDK example query to include it.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
packages/ensnode-sdk/src/graphql-api/example-queries.ts Extends the “DomainByName” example query to request ENSv1Domain.manager.
packages/ensnode-schema/src/schemas/ensv2.schema.ts Adds v1Domain.managerId and relations_v1Domain.manager -> account.
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts Persists managerId and stops deleting v1 domains on zero-address transfer; always re-materializes effective owner.
apps/ensapi/src/graphql-api/schema/domain.ts Adds ENSv1Domain.manager: Account field resolved from managerId.

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

You can also share your feedback on Copilot code review. Take the survey.

Copy link
Contributor

@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.

Caution

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

⚠️ Outside diff range comments (1)
packages/ensnode-schema/src/schemas/ensv2.schema.ts (1)

182-186: 🧹 Nitpick | 🔵 Trivial

Consider adding an index on managerId for query performance.

If the API will support filtering or ordering domains by manager, an index similar to byOwner may be beneficial:

   byParent: index().on(t.parentId),
   byOwner: index().on(t.ownerId),
   byLabelHash: index().on(t.labelHash),
+  byManager: index().on(t.managerId),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ensnode-schema/src/schemas/ensv2.schema.ts` around lines 182 - 186,
Add an index for managerId to improve queries that filter/order by manager:
inside the schema index block (the arrow function that returns
byParent/byOwner/byLabelHash) add a new entry like byManager that uses
index().on(t.managerId) so the schema includes a `byManager` index for the
managerId field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/ensnode-schema/src/schemas/ensv2.schema.ts`:
- Around line 182-186: Add an index for managerId to improve queries that
filter/order by manager: inside the schema index block (the arrow function that
returns byParent/byOwner/byLabelHash) add a new entry like byManager that uses
index().on(t.managerId) so the schema includes a `byManager` index for the
managerId field.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fcd2a6f7-2954-4f2e-b860-e92b950ea4f1

📥 Commits

Reviewing files that changed from the base of the PR and between ef7ab2a and 4498a27.

📒 Files selected for processing (4)
  • apps/ensapi/src/graphql-api/schema/domain.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts
  • packages/ensnode-schema/src/schemas/ensv2.schema.ts
  • packages/ensnode-sdk/src/graphql-api/example-queries.ts

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR adds first-class tracking of the ENSv1 Registry manager (the owner() in the ENSv1 Registry contract, distinct from the materialized effective owner) and fixes an incorrect deletion of domains when a Transfer event transfers ownership to the zero address.

Key changes:

  • Adds a nullable managerId column to v1_domains and a Drizzle manager relation pointing to Account, consistent with the existing ownerId/owner pattern.
  • handleNewOwner now runs a second UPDATE after the INSERT ... ON CONFLICT DO NOTHING to record the managerId.
  • handleTransfer no longer deletes the domain on zero-address transfers; instead it updates managerId (null for zero address) and always calls materializeENSv1DomainEffectiveOwner — which safely handles zero via ensureAccount's no-op guard and interpretAddress returning null.
  • Exposes the manager field on ENSv1DomainRef in the GraphQL API and updates the example SDK query.
  • One style consideration: The managerId column lacks an index, unlike the analogous ownerId / byOwner index. This asymmetry should be considered if a future "domains by manager" query is introduced.

Confidence Score: 4/5

  • Safe to merge. The logic is sound and the fix for incorrect deletion on zero-address transfers is a clear correctness improvement.
  • The PR correctly implements tracking of the ENSv1 Registry manager and fixes the zero-address transfer bug. All patterns are consistent with existing code. One style consideration: the missing byManager index for consistency with the byOwner pattern, which may be needed if future "domains by manager" queries are introduced. This is not a blocking issue but worth considering for future consistency.
  • packages/ensnode-schema/src/schemas/ensv2.schema.ts — consider adding byManager index to match the byOwner pattern

Sequence Diagram

sequenceDiagram
    participant ENSv1Registry as ENSv1 Registry Contract
    participant Handler as ENSv1Registry Handler
    participant DB as Ponder DB (v1_domains)
    participant AccountDB as Ponder DB (accounts)

    Note over ENSv1Registry,AccountDB: NewOwner Event (subdomain creation / re-assignment)
    ENSv1Registry->>Handler: NewOwner(node, label, owner)
    Handler-->>Handler: early-return if owner == zeroAddress
    Handler->>DB: INSERT v1Domain (id, parentId, labelHash) ON CONFLICT DO NOTHING
    Handler->>DB: UPDATE v1Domain SET managerId = interpretAddress(owner)
    Handler->>AccountDB: ensureAccount(owner)
    Handler->>DB: UPDATE v1Domain SET ownerId = interpretAddress(owner)

    Note over ENSv1Registry,AccountDB: Transfer Event (ownership change, incl. zero address)
    ENSv1Registry->>Handler: Transfer(node, owner)
    Handler-->>Handler: early-return if node == ROOT_NODE
    Handler->>DB: UPDATE v1Domain SET managerId = interpretAddress(owner)
    Note right of DB: null if owner == zeroAddress
    Handler->>AccountDB: ensureAccount(owner) — no-op for zeroAddress
    Handler->>DB: UPDATE v1Domain SET ownerId = interpretAddress(owner)
    Note right of DB: Domain persists (no longer deleted on zero transfer)
Loading

Comments Outside Diff (1)

  1. packages/ensnode-schema/src/schemas/ensv2.schema.ts, line 182-186 (link)

    The byOwner index on ownerId is present, but the new managerId field doesn't have a corresponding byManager index. If a future query path needs to find all domains managed by a given account (e.g., an Account.managedDomains GraphQL field), a full table scan will be required.

    Consider adding a byManager index for consistency:

Last reviewed commit: 4498a27

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 Updates here look good to me 👍

Copy link
Contributor

@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: 1

Caution

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

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

82-93: ⚠️ Potential issue | 🟠 Major

Foreign key constraint may be violated: ensureAccount should be called before setting rootRegistryOwnerId.

The rootRegistryOwnerId column references account.id (per the relation in ensv2.schema.ts). At lines 85 and 113, rootRegistryOwnerId is set to interpretAddress(owner), but ensureAccount is only called later inside materializeENSv1DomainEffectiveOwner.

In handleNewOwner, the early return at line 52 (if owner is zeroAddress) guarantees that owner is non-zero when reaching line 85, so interpretAddress(owner) will be a non-null address. If the account doesn't exist yet in the database, this will cause a foreign key violation.

The same issue applies to handleTransfer at line 113, though the impact is less severe if owner can be zeroAddress (which nullifies the FK constraint).

🔧 Proposed fix: ensure account exists before setting rootRegistryOwnerId

Import ensureAccount and call it before the update:

+import { ensureAccount } from "@/lib/ensv2/account-db-helpers";
...
    // upsert domain
    await context.db
      .insert(schema.v1Domain)
      .values({ id: domainId, parentId, labelHash })
      .onConflictDoNothing();

+   // ensure account exists before setting rootRegistryOwnerId
+   await ensureAccount(context, owner);
+
    // update rootRegistryOwner
    await context.db
      .update(schema.v1Domain, { id: domainId })
      .set({ rootRegistryOwnerId: interpretAddress(owner) });

    // materialize domain owner
    ...
    await materializeENSv1DomainEffectiveOwner(context, domainId, owner);

Apply the same fix to handleTransfer.

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

In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts` around
lines 82 - 93, The update to rootRegistryOwnerId must ensure the referenced
account exists first: in handleNewOwner (and likewise in handleTransfer) call
ensureAccount(context, interpretAddress(owner)) before updating schema.v1Domain
with rootRegistryOwnerId, then perform the await context.db.update(...).set({
rootRegistryOwnerId: interpretAddress(owner) });; you can still call
materializeENSv1DomainEffectiveOwner afterwards, but ensureAccount is invoked
prior to setting rootRegistryOwnerId to avoid foreign key violations (refer to
ensureAccount, interpretAddress, handleNewOwner, handleTransfer, and
materializeENSv1DomainEffectiveOwner).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts`:
- Around line 110-121: The update to rootRegistryOwnerId is happening before the
account is guaranteed to exist, which can violate the FK when
interpretAddress(owner) is non-null; move or change the logic so that
materializeENSv1DomainEffectiveOwner (which calls ensureAccount) runs before
updating rootRegistryOwnerId, or explicitly call ensureAccount for the owner
address prior to the context.db.update; reference the update call that sets
rootRegistryOwnerId and the function materializeENSv1DomainEffectiveOwner (and
ensureAccount) to locate where to reorder or add the ensureAccount call, and
preserve the current handling where interpretAddress(owner) may return null for
the zero address.

---

Outside diff comments:
In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts`:
- Around line 82-93: The update to rootRegistryOwnerId must ensure the
referenced account exists first: in handleNewOwner (and likewise in
handleTransfer) call ensureAccount(context, interpretAddress(owner)) before
updating schema.v1Domain with rootRegistryOwnerId, then perform the await
context.db.update(...).set({ rootRegistryOwnerId: interpretAddress(owner) });;
you can still call materializeENSv1DomainEffectiveOwner afterwards, but
ensureAccount is invoked prior to setting rootRegistryOwnerId to avoid foreign
key violations (refer to ensureAccount, interpretAddress, handleNewOwner,
handleTransfer, and materializeENSv1DomainEffectiveOwner).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 57d5638e-e95a-42df-8867-56442bce5950

📥 Commits

Reviewing files that changed from the base of the PR and between 4498a27 and b3d2bfe.

📒 Files selected for processing (5)
  • .changeset/strong-baboons-help.md
  • apps/ensapi/src/graphql-api/schema/domain.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts
  • packages/ensnode-schema/src/schemas/ensv2.schema.ts
  • packages/ensnode-sdk/src/graphql-api/example-queries.ts

@shrugs shrugs merged commit 10b368a into main Mar 9, 2026
16 checks passed
@shrugs shrugs deleted the feat/ensv1-manager branch March 9, 2026 17:29
@github-actions github-actions bot mentioned this pull request Mar 9, 2026
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: Index ENSv1 manager from ENS Registry

3 participants