Skip to content

ENSv2 Plugin Events#1744

Open
shrugs wants to merge 19 commits intomainfrom
feat/ensv2-events
Open

ENSv2 Plugin Events#1744
shrugs wants to merge 19 commits intomainfrom
feat/ensv2-events

Conversation

@shrugs
Copy link
Collaborator

@shrugs shrugs commented Mar 9, 2026

closes #1674

I was trying to debug some event decoding issues, which necessitated making sure that the devnet and my ABIs were in sync with each other, so this also includes an update to the commit of devnet that we're integration testing against.

Reviewer Focus (Read This First)

the main areas to review:

  1. event-db-helpers expansionensureDomainEvent, ensureResolverEvent, ensurePermissionsEvent now push events to the appropriate join tables
  2. find-events resolver and where filters — new cursor-based pagination for events with topic0_in, timestamp_gte/lte, from filters. the through: { table, scope } pattern for narrowing via join tables
  3. update ens-test-env to latest devnet — large surface area of ABI updates. these are built and then copied from the ens contract repos

Problem & Motivation

  • ens alpha apps want event history for domains and resolvers
  • Registration.start was previously removed from the GraphQL API in favor of joining through registration.event.timestamp, making queries slower and more complex
  • ens-test-env datasource was out of date

What Changed (Concrete)

  1. event history tracking — all ensv2 handlers now push events to domain/resolver/permissions join tables. new handlers for ENSv1Registry (NewTTL, NewResolver) and shared Resolver events.
  2. Registration.start materialized — stored on the registration row, exposed in GraphQL, used directly for ordering (removes event table join from with-ordering-metadata).
  3. events API — Account.events field added. where filters (topic0_in, timestamp_gte/lte, from) on all *.events connections. resolveFindEvents queries event table directly, optionally narrowed via join table.
  4. schema/infra — event table expanded (topic0, topics, data, from/to), cursor utilities consolidated, Event GraphQL type completed.
  5. ens-test-env update — ABI updates across contracts, new UniversalResolverV2

Design & Planning

  • event tracking follows join-table pattern
  • resolveFindEvents design iterated through: join-table-first → scope-first → always-query-event-table with optional join. final design keeps the query plan simple and avoids conditional FROM clause complexity.
  • where filter design follows the find-domains pattern (input types → internal filter shape → SQL conditions)

Self-Review

  • Bugs caught: none significant
  • Logic simplified: removed unnecessary event table join from with-ordering-metadata after materializing registration.start
  • Naming / terminology improved: through: { table, scope } reads clearly at callsites
  • Dead or unnecessary code removed: old cursors.ts in schema/ replaced by shared cursors lib

Downstream & Consumer Impact

  • Public APIs affected:
    • Registration.start field added (non-breaking, new field)
    • Account.events connection added (non-breaking, new field)
    • Domain.events, Resolver.events, Permissions.events now accept where arg (non-breaking, optional)
    • Event type gains blockNumber, transactionIndex, to, topics, data fields (non-breaking)
    • new input types: EventsWhereInput, AccountEventsWhereInput
  • Docs updated: changeset added

Testing Evidence

  • Testing performed: manual devnet integration testing
  • Known gaps: no integration tests for event pagination or where filters yet
  • What reviewers have to reason about manually: correctness of SQL composition in resolveFindEvents

Risk Analysis

  • Risk areas: could run into issues at runtime with the find-events module since it's not fully integration tested
  • Mitigations: ensv2 plugin is alpha
  • 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)

Copilot AI review requested due to automatic review settings March 9, 2026 21:38
@vercel
Copy link
Contributor

vercel bot commented Mar 9, 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 11, 2026 5:02pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
ensnode.io Skipped Skipped Mar 11, 2026 5:02pm
ensrainbow.io Skipped Skipped Mar 11, 2026 5:02pm

@changeset-bot
Copy link

changeset-bot bot commented Mar 9, 2026

🦋 Changeset detected

Latest commit: bee45fe

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

This PR includes changesets to release 19 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
@ensnode/integration-test-env Patch

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

This pull request implements event history tracking for ENSv2 by introducing a centralized pagination cursors utility, extending the Event schema with filtering support, creating domain/resolver/permissions event join tables, adding paginated event query resolvers, and integrating event tracking throughout domain, resolver, account, and permissions GraphQL types and indexer handlers.

Changes

Cohort / File(s) Summary
Pagination & Cursor Utilities
apps/ensapi/src/graphql-api/lib/cursors.ts, apps/ensapi/src/graphql-api/lib/connection-helpers.ts, apps/ensapi/src/graphql-api/lib/find-domains/domain-cursor.ts, apps/ensapi/src/graphql-api/lib/find-domains/domain-cursor.test.ts, apps/ensapi/src/graphql-api/schema/cursors.ts
Centralized pagination cursor encoding/decoding in shared lib utility using superjson and base64; updated domain cursor to delegate to shared cursors; removed legacy schema cursors module.
Event Schema & Database
apps/ensapi/src/graphql-api/schema/event.ts, packages/ensnode-schema/src/schemas/ensv2.schema.ts, apps/ensindexer/src/lib/ensv2/event-db-helpers.ts
Extended Event type with blockNumber, transactionIndex, to, topics, data fields; added EventsWhereInput and AccountEventsWhereInput filter types; created domainEvent, resolverEvent, permissionsEvent join tables; refactored event-db-helpers to support event-domain/resolver/permissions linking.
Event Querying Resolver
apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts
New resolveFindEvents resolver providing paginated event queries with optional where filtering (topic0_in, timestamp_gte/lte, from) and through-join scoping for domain/resolver/permissions relationships.
GraphQL Type Event Integration
apps/ensapi/src/graphql-api/schema/domain.ts, apps/ensapi/src/graphql-api/schema/resolver.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
Added events connection fields to Domain, Resolver, Account, and Permissions; added start field to Registration; integrated resolveFindEvents with through-join and where filtering.
Pagination Constants & Configuration
apps/ensapi/src/graphql-api/schema/constants.ts, apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts
Extracted PAGINATION_DEFAULT_PAGE_SIZE and PAGINATION_DEFAULT_MAX_SIZE constants; updated cursor payload to include defaultSize, maxSize, and args; adjusted find-domains resolver pagination logic.
ENSv1 Event Handlers
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts, apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts, apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts, apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts
Added event tracking via ensureDomainEvent for Transfer, NewOwner, NewResolver, NewTTL, NameRegistered, NameRenewed, NameWrapped, FusesSet, ExpiryExtended; added start timestamp to registration inserts.
ENSv2 Event Handlers
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts, apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts, apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts, apps/ensindexer/src/plugins/ensv2/handlers/shared/Resolver.ts
Integrated domain/resolver/permissions event tracking into ENSv2 handlers; added Resolver event handler module; ensureDomainEvent/ensureResolverEvent/ensurePermissionsEvent calls after relevant mutations.
Indexer Helpers & Types
apps/ensindexer/src/lib/get-this-account-id.ts, apps/ensindexer/src/lib/ponder-helpers.ts, apps/ensindexer/src/lib/subgraph/db-helpers.ts, apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts
Updated LogEvent to LogEventBase type; refactored ponder-helpers to define FilterLogEvents and LogEventBase types; enhanced heal-addr-reverse logic to attempt contract address healing.
Plugin Configuration
apps/ensindexer/src/plugins/ensv2/event-handlers.ts, apps/ensindexer/src/plugins/ensv2/plugin.ts
Registered Resolver handlers; added Resolver contract configuration and block range setup in ENSv2 plugin.
ABI Updates: ETHRegistrar & Registry
packages/datasources/src/abis/ensv2/ETHRegistrar.ts, packages/datasources/src/abis/ensv2/Registry.ts, packages/datasources/src/abis/ensv2/EnhancedAccessControl.ts
Replaced REGISTRY function with constructor; added role-based access control functions and errors; added RentPriceOracle support; updated Registry events (NameRegistered, NameReserved, NameUnregistered) with labelHash and sender fields; added getParent function.
ABI Updates: Universal Resolvers & Root Contracts
packages/datasources/src/abis/ensv2/UniversalResolverV2.ts, packages/datasources/src/abis/root/UniversalResolverV1.ts, packages/datasources/src/abis/root/BaseRegistrar.ts, packages/datasources/src/abis/root/NameWrapper.ts, packages/datasources/src/abis/root/Registry.ts, packages/datasources/src/abis/shared/UniversalResolver.ts
Added UniversalResolverV2 ABI; renamed UniversalResolver to UniversalResolverV1 with updated constructor/functions; updated BaseRegistrar ABI with constructor and new events; expanded NameWrapper with approval/upgrade functions; added unified UniversalResolverABI merger.
ABI Updates: Basenames & LineaNames
packages/datasources/src/abis/basenames/*, packages/datasources/src/abis/lineanames/*
Applied Abi type constraint (as const satisfies Abi) across all basenames and lineanames ABI files.
ABI Updates: Shared & Other ABIs
packages/datasources/src/abis/shared/AbstractReverseResolver.ts, packages/datasources/src/abis/shared/StandaloneReverseRegistrar.ts, packages/datasources/src/abis/shared/LegacyPublicResolver.ts, packages/datasources/src/abis/shared/Resolver.ts, packages/datasources/src/abis/root/LegacyEthRegistrarController.ts, packages/datasources/src/abis/root/UnwrappedEthRegistrarController.ts, packages/datasources/src/abis/root/WrappedEthRegistrarController.ts, packages/datasources/src/abis/root/UniversalRegistrarRenewalWithReferrer.ts, packages/datasources/src/abis/threedns/ThreeDNSToken.ts, packages/datasources/src/abis/seaport/Seaport1.5.ts
Applied Abi type constraint across shared and root ABI files; added chainRegistrar function; updated event/function signatures in reverse resolvers.
Datasource Configuration Updates
packages/datasources/src/ens-test-env.ts, packages/datasources/src/mainnet.ts, packages/datasources/src/sepolia.ts, packages/datasources/src/sepolia-v2.ts, packages/datasources/src/index.ts
Updated ENSTestEnv with new devnet addresses and added ENSv1RegistryOld/ENSv1Registry; migrated UniversalResolver to UniversalResolverV1 across mainnet/sepolia configs; exported UniversalResolverABI from shared module; added DefaultPublicResolver5 entries.
SDK Example Queries & Documentation
packages/ensnode-sdk/src/graphql-api/example-queries.ts, packages/integration-test-env/README.md, packages/integration-test-env/src/orchestrator.ts
Added Domain.events, Account.events, Resolver.events, and Permissions.events GraphQL query examples with where filtering; updated devnet image tag; added getDatasourceContract import.
Ordering & Domain Queries
apps/ensapi/src/graphql-api/lib/find-domains/layers/with-ordering-metadata.ts
Updated registration timestamp source from event.timestamp to registration.start (materialized field).
Tests & Changesets
apps/ensapi/src/graphql-api/schema/account.integration.test.ts, .changeset/breezy-corners-tickle.md, .changeset/shy-wolves-judge.md, .changeset/wide-chicken-dream.md
Added placeholder test blocks for Account.events; documented Registration.start availability and events filtering (topic0_in, timestamp_gte/lte, from) for Domain/Resolver/Account/Permissions connections.

Sequence Diagrams

sequenceDiagram
    participant Indexer as Indexer Handler
    participant EventDB as Event DB Helper
    participant EventTable as events Table
    participant JoinTable as domain_event Table
    
    Indexer->>EventDB: ensureDomainEvent(context, event, domainId)
    EventDB->>EventTable: ensureEvent(context, event)
    EventTable->>EventTable: Insert/upsert event with blockNumber, topics, data, timestamp
    EventTable-->>EventDB: event.id
    EventDB->>JoinTable: Insert (domainId, eventId) link
    JoinTable-->>EventDB: Link created
    EventDB-->>Indexer: ✓ Confirmed
Loading
sequenceDiagram
    participant Client as GraphQL Client
    participant Resolver as resolveFindEvents
    participant DB as Database
    participant Cursor as Cursor Encoder
    
    Client->>Resolver: Query Domain.events(where: {topic0_in: [...], timestamp_gte: X}, first: 10, after: cursor)
    Resolver->>Cursor: decode(after)
    Cursor-->>Resolver: Previous cursor state
    Resolver->>DB: SELECT events WHERE domainId=? AND (where filters) ORDER BY timestamp, chainId LIMIT 11
    DB-->>Resolver: [events]
    Resolver->>Cursor: encode(lastEvent)
    Cursor-->>Resolver: nextCursor
    Resolver-->>Client: Connection {edges, pageInfo{endCursor, hasNextPage}}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

ensapi, ensindexer, datasources, schema

Poem

🐰 Events hop through our schema now,
Domain trails and resolver vows,
Cursor-encoded histories flow,
Filtered by time and address glow—
The indexed path remembers all,
Each mutation answers the call!

✨ 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/ensv2-events

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 ENSv2 “Event” storage + GraphQL exposure so Domains/Resolvers/Registries can surface an on-chain audit trail via cursor pagination, aligning with the long-term “history” needs described in #1674.

Changes:

  • Expanded the events table to store full log metadata (block/tx indices, topics, data) and added join tables (domain_events, resolver_events, registry_events).
  • Added ENSIndexer helpers/handlers to persist events and attach ENSv1 registry events to Domain history.
  • Added GraphQL *.events connections plus new keyset cursor utilities (superjson base64 cursors) and pagination plumbing.

Reviewed changes

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

Show a summary per file
File Description
packages/ensnode-schema/src/schemas/ensv2.schema.ts Expands event storage and adds join tables for entity↔event relationships.
packages/datasources/src/abis/ensv2/Registry.ts Updates ENSv2 Registry ABI event/function shapes (sender/indexed fields, new events).
packages/datasources/src/abis/ensv2/EnhancedAccessControl.ts ABI param rename (rolesBitmaproleBitmap).
packages/datasources/src/abis/ensv2/ETHRegistrar.ts Large ABI refresh (constructor, errors, events, view/mutating fns).
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts Adds event-to-domain-history wiring and indexes additional ENSv1 registry events.
apps/ensindexer/src/lib/ensv2/event-db-helpers.ts Implements ensureEvent (richer metadata) + ensureDomainEvent.
apps/ensapi/src/graphql-api/schema/event.ts Exposes new Event fields (blockNumber, txIndex, to, topics, data).
apps/ensapi/src/graphql-api/schema/domain.ts Adds Domain.events connection.
apps/ensapi/src/graphql-api/schema/resolver.ts Adds Resolver.events connection.
apps/ensapi/src/graphql-api/schema/registry.ts Adds Registry.events connection.
apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts New reusable resolver for paginated event connections via join tables.
apps/ensapi/src/graphql-api/lib/find-events/event-cursor.ts Defines composite event cursor encode/decode.
apps/ensapi/src/graphql-api/lib/cursors.ts Centralizes base64(superjson) cursor encoding/decoding.
apps/ensapi/src/graphql-api/schema/constants.ts Moves cursor import + centralizes default/max page sizes.
apps/ensapi/src/graphql-api/lib/connection-helpers.ts Updates to use new shared cursors util.
apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts Switches domain pagination to shared page-size constants + DomainCursors.
apps/ensapi/src/graphql-api/lib/find-domains/domain-cursor.ts Uses shared cursors util; renames helper to DomainCursors.
apps/ensapi/src/graphql-api/lib/find-domains/domain-cursor.test.ts Updates tests to use DomainCursors.
apps/ensapi/src/graphql-api/schema/cursors.ts Removes old cursor helper (now replaced by lib/cursors).

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

Actionable comments posted: 8

🤖 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/ensapi/src/graphql-api/lib/cursors.ts`:
- Around line 14-17: The thrown Error message in the cursor decoding catch block
is too specific ("failed to decode event cursor") for a shared utility; update
the message to be generic (e.g., "Invalid cursor: failed to decode cursor. The
cursor may be malformed or from an incompatible query.") in the catch inside the
cursor decoding function (the catch that currently throws the "Invalid cursor:
failed to decode event cursor..." error – locate the cursor decode utility,
e.g., decodeCursor/parseCursor or the catch in cursors.ts) so it no longer
references "event".

In `@apps/ensapi/src/graphql-api/lib/find-events/event-cursor.ts`:
- Around line 17-19: EventCursors.decode currently returns
cursors.decode<EventCursor>(cursor) without runtime validation; mirror the
pattern used in DomainCursors.decode by wrapping the decoded value in a
validation/guard and throwing a clear error if it doesn't match the EventCursor
shape. Update the EventCursors object (specifically the decode implementation)
to call cursors.decode, validate the result against the EventCursor structure
(or use an existing type guard/validator), and throw a descriptive error when
validation fails, similar to the TODO noted in domain-cursor.ts and
DomainCursors.decode.

In `@apps/ensapi/src/graphql-api/schema/domain.ts`:
- Around line 247-257: Domain.events currently lacks where/order args (left as
TODO); add the same connection args used by the registrations connection and
pass them into resolveFindEvents. Update the Domain.events field to accept
filtering/sorting args (e.g., where, order, first/after) matching the connection
input types used by registrations, ensure the resolver call
resolveFindEvents(schema.domainEvent, eq(schema.domainEvent.domainId,
parent.id), args) forwards those args unchanged, and add/update unit tests for
events filtering/ordering and schema types for EventRef to reflect the new args.

In `@apps/ensindexer/src/lib/ensv2/event-db-helpers.ts`:
- Around line 28-33: The current invariant throws an Error when event.log.topics
contains null, which is blocking CI; change this to log a non-fatal warning and
skip processing that log instead of throwing so indexing continues. Replace the
throw in the block that checks event.log.topics.some(topic => topic === null)
with a warning call (e.g., processLogger.warn or a module logger) that includes
the same message/context and the toJson(event.log.topics) payload, then
return/continue from the enclosing function to skip that malformed event;
alternatively only throw in a dev-only branch (NODE_ENV==='test' or an explicit
feature flag) if you must preserve the invariant in local dev. Ensure you update
all references to event.log.topics and keep the descriptive message for
debugging.
- Around line 67-69: The insert in ensureDomainEvent is not idempotent and will
fail on duplicate (domainId,eventId); update ensureDomainEvent to use the same
dedup pattern as ensureEvent by performing the insert into schema.domainEvent
with conflict handling (e.g., add an onConflictDoNothing() / equivalent on the
context.db.insert call) so repeated calls for the same domainId and eventId are
no-ops and do not throw.

In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts`:
- Around line 131-159: Both handleNewTTL and handleNewResolver call
makeENSv1DomainId(node) and persist history via ensureDomainEvent even when node
=== ROOT_NODE; add an early return to skip handling when node equals ROOT_NODE
to avoid creating history for an unmodeled root domain. Specifically, in both
functions (handleNewTTL and handleNewResolver) check if event.args.node ===
ROOT_NODE (or compare node to ROOT_NODE) and return immediately before computing
domainId or calling ensureDomainEvent.

In `@packages/ensnode-schema/src/schemas/ensv2.schema.ts`:
- Around line 132-149: The join tables domainEvent, resolverEvent, and
registryEvent currently only define composite primary keys (entityId, eventId),
which orders the index by entity first and hurts performance for joins on
eventId; add a secondary index on eventId for each join table by updating the
onchainTable definitions (domainEvent, resolverEvent, registryEvent) to include
an index referencing t.eventId (e.g., add an index/secondaryIndex entry using
t.eventId) so that joins like .innerJoin(schema.event, eq(joinTable.eventId,
schema.event.id)) can use an eventId-prefixed index.
- Around line 83-90: The doc comment describing the Events table is cut off
("These join tables may store additional"); update the comment in
ensv2.schema.ts (the block describing Events, DomainEvent, ResolverEvent,
Registration, Renewal) to complete that sentence—e.g., state that join tables
may store additional relationship metadata (such as event-specific fields,
timestamps, blockNumber/transactionHash, or link attributes) so readers
understand what extra data DomainEvent/ResolverEvent can hold; keep the rest of
the paragraph unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 96533aea-2bf7-4f52-bd5c-7bc1feb37f7d

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa8326 and 38ab526.

📒 Files selected for processing (19)
  • apps/ensapi/src/graphql-api/lib/connection-helpers.ts
  • apps/ensapi/src/graphql-api/lib/cursors.ts
  • apps/ensapi/src/graphql-api/lib/find-domains/domain-cursor.test.ts
  • apps/ensapi/src/graphql-api/lib/find-domains/domain-cursor.ts
  • apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts
  • apps/ensapi/src/graphql-api/lib/find-events/event-cursor.ts
  • apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts
  • apps/ensapi/src/graphql-api/schema/constants.ts
  • apps/ensapi/src/graphql-api/schema/cursors.ts
  • apps/ensapi/src/graphql-api/schema/domain.ts
  • apps/ensapi/src/graphql-api/schema/event.ts
  • apps/ensapi/src/graphql-api/schema/registry.ts
  • apps/ensapi/src/graphql-api/schema/resolver.ts
  • apps/ensindexer/src/lib/ensv2/event-db-helpers.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts
  • packages/datasources/src/abis/ensv2/ETHRegistrar.ts
  • packages/datasources/src/abis/ensv2/EnhancedAccessControl.ts
  • packages/datasources/src/abis/ensv2/Registry.ts
  • packages/ensnode-schema/src/schemas/ensv2.schema.ts
💤 Files with no reviewable changes (1)
  • apps/ensapi/src/graphql-api/schema/cursors.ts

Copilot AI review requested due to automatic review settings March 10, 2026 21:36
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io March 10, 2026 21:36 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io March 10, 2026 21:36 Inactive
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: 10

♻️ Duplicate comments (2)
apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts (1)

54-56: ⚠️ Potential issue | 🟠 Major

Cast bigint cursor values in eventCursorWhere.

The tuple comparison is still interpolating bigint-backed cursor fields without explicit ::bigint casts. That can break before/after pagination at runtime when PostgreSQL cannot infer the tuple parameter types.

🛠️ Proposed fix
 function eventCursorWhere(op: ">" | "<", key: EventCursor): SQL {
   const [tCol, cCol, bCol, txCol, lCol, idCol] = EVENT_SORT_COLUMNS;
-  return sql`(${tCol}, ${cCol}, ${bCol}, ${txCol}, ${lCol}, ${idCol}) ${sql.raw(op)} (${key.timestamp}, ${key.chainId}, ${key.blockNumber}, ${key.transactionIndex}, ${key.logIndex}, ${key.id})`;
+  return sql`(${tCol}, ${cCol}, ${bCol}, ${txCol}, ${lCol}, ${idCol}) ${sql.raw(op)} (${sql`${key.timestamp}::bigint`}, ${key.chainId}, ${sql`${key.blockNumber}::bigint`}, ${key.transactionIndex}, ${key.logIndex}, ${key.id})`;
 }

Run this to verify the helper still lacks bigint casts and to compare with any existing casted tuple comparisons in the repo:

#!/bin/bash
set -euo pipefail

echo "Inspect current tuple comparison in eventCursorWhere:"
sed -n '54,56p' apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts

echo
echo "Search for explicit ::bigint casts in similar SQL fragments:"
rg -n -C2 '::bigint|eventCursorWhere|row-value|tuple comparison' apps/ensapi/src/graphql-api/lib
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts` around
lines 54 - 56, The tuple comparison in eventCursorWhere is interpolating
bigint-backed cursor fields without explicit ::bigint casts; update the SQL
returned by eventCursorWhere to append ::bigint to each bigint cursor
interpolation (e.g., cast key.chainId, key.blockNumber, key.transactionIndex,
key.logIndex, and key.id) so the RHS tuple types match the DB column types from
EVENT_SORT_COLUMNS and avoid Postgres ambiguity during before/after pagination.
apps/ensindexer/src/lib/ensv2/event-db-helpers.ts (1)

27-35: ⚠️ Potential issue | 🔴 Critical

Validate topic0 after filtering out null topics.

The precondition only checks that the original array is non-empty. If event.log.topics is [null], Line 35 turns it into [] and Line 59 still inserts topic0: topics[0]. That can fail the insert or persist a malformed event row.

🔧 Minimal fix
-  const topics = event.log.topics.filter((topic) => topic !== null) as typeof event.log.topics;
+  const topics = event.log.topics.filter((topic): topic is Hash => topic !== null);
+  const [topic0, ...restTopics] = topics;
+  if (topic0 === undefined) {
+    throw new Error(`Invariant: All events indexed via ensureEvent must have a non-null topic0.`);
+  }

   await context.db
     .insert(schema.event)
     .values({
       id: event.id,
@@
-      topic0: topics[0],
-      topics,
+      topic0,
+      topics: [topic0, ...restTopics],
       data: event.log.data,
     })

Also applies to: 59-60

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

In `@apps/ensindexer/src/lib/ensv2/event-db-helpers.ts` around lines 27 - 35, The
code in ensureEvent uses hasTopics(event.log.topics) then filters nulls into
topics but doesn't re-check that topics[0] (topic0) exists before persisting;
add a validation after building const topics = event.log.topics.filter(...) to
ensure topics.length > 0 and throw a clear error (or return) if empty, so that
subsequent use of topics[0] (the topic0 inserted later) cannot be undefined;
update any related insertion logic that sets topic0 to use this validated
topics[0].
🤖 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/lib/ensv2/event-db-helpers.ts`:
- Around line 68-95: The relation helpers compute eventId but don't return it;
update ensureDomainEvent, ensureResolverEvent, and ensurePermissionsEvent to
return the eventId (change their signatures to return Promise<EventId> or
appropriate type) and keep the existing insert logic (await the insert
.onConflictDoNothing()) before returning eventId so callers can reuse the id
instead of calling ensureEvent again.

In `@apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts`:
- Around line 62-72: Wrap the call to context.client.getTransactionReceipt in a
try-catch so failures don't abort the function; if the call throws, swallow or
log the error and continue to the existing trace-based fallback logic (the code
that calls maybeHealLabelByTxTrace / trace-based healing). Specifically, protect
the block that assigns receipt and then calls
maybeHealLabelByAddrReverseSubname(labelHash, receipt.contractAddress) so that
any thrown error only skips the contractAddress-based healing path but still
allows the trace-based healing code to run.

In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts`:
- Around line 182-184: The ensureDomainEvent call is currently executed before
the handler's state mutations; move the call to after the entire if/else block
that updates or inserts registrations so the event is recorded only after
db.update or insertLatestRegistration completes—locate the ensureDomainEvent
invocation in the NameWrapper handler and relocate it to the end of the function
(after the block ending around the place referenced as line 257) so it follows
both the BaseRegistrar/NameWrapper mutation paths.

In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts`:
- Around line 144-145: The destructured but unused "sender" in the ENSv2Registry
event handler should be removed instead of silencing the linter: update the
destructuring of event.args (the line using const { tokenId, newExpiry: expiry,
sender } = event.args) to only extract used fields (e.g., const { tokenId,
newExpiry: expiry } = event.args) and delete the accompanying biome-ignore
lint/correctness/noUnusedVariables comment; if "sender" will be needed later,
keep it and use it where appropriate, otherwise remove both the variable and the
lint suppression.

In `@packages/datasources/src/abis/root/BaseRegistrar.ts`:
- Around line 1-2: Wrap the exported ABI array with a type-level guard by
appending "as const satisfies Abi" to the declaration of BaseRegistrar so
TypeScript validates the ABI shape at the declaration site (ensure the Abi type
is imported/available); apply the same change to the other similar ABI export
around the other occurrence referenced in the comment.

In `@packages/ensnode-schema/src/schemas/ensv2.schema.ts`:
- Around line 133-149: The exported table variable name is inconsistent: rename
permissionsEvents to permissionsEvent to match the singular naming used by
domainEvent and resolverEvent; update the export/identifier for the onchainTable
call that defines the permissions join table (the constant currently declared as
permissionsEvents that constructs the table with permissionsId and eventId and
primaryKey) and then update all references/usages/imports of permissionsEvents
throughout the codebase to the new permissionsEvent identifier so builds and
imports remain correct.
- Line 5: Remove the unused import AccountIdString from the imports in
ensv2.schema.ts: locate the import list that currently includes AccountIdString
and delete that identifier so the file no longer imports AccountIdString (ensure
no other code references AccountIdString in this file before removing).

In `@packages/ensnode-sdk/src/graphql-api/example-queries.ts`:
- Around line 350-355: The test resolver object under ENSNamespaceIds.EnsTestEnv
uses an arbitrary lowercase address with a comment "idk, random resolver";
replace it with a known resolver address from the test env (use the same helper
pattern as getDatasourceContract where available) or, if a concrete resolver
isn't available, replace the comment with a TODO explaining why and how to
supply a real resolver later. Also normalize the address to checksummed format
(matching DEVNET_DEPLOYER style) and update the inline comment to clearly state
provenance (e.g., "known test resolver from fixture" or "TODO: replace with real
test resolver").
- Around line 190-198: The GraphQL operation name inside the template literal is
wrong: replace the operation name "AccountDomains" with "AccountEvents" in the
query string (the template that starts with query AccountDomains($address:
Address!) { ... }) so the operation name matches the query body for fetching
account events; ensure the updated operation name is used consistently if
referenced elsewhere in the same example (search for "AccountDomains" in
example-queries.ts and rename to "AccountEvents").

In `@packages/integration-test-env/README.md`:
- Around line 9-12: Add a language identifier to the fenced code block
containing the registry string "ghcr.io/ensdomains/contracts-v2:main-cb8e11c" so
markdownlint MD040 stops flagging it; change the opening backticks from ``` to
```text (i.e., mark the block as text) while keeping the block content
unchanged.

---

Duplicate comments:
In `@apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts`:
- Around line 54-56: The tuple comparison in eventCursorWhere is interpolating
bigint-backed cursor fields without explicit ::bigint casts; update the SQL
returned by eventCursorWhere to append ::bigint to each bigint cursor
interpolation (e.g., cast key.chainId, key.blockNumber, key.transactionIndex,
key.logIndex, and key.id) so the RHS tuple types match the DB column types from
EVENT_SORT_COLUMNS and avoid Postgres ambiguity during before/after pagination.

In `@apps/ensindexer/src/lib/ensv2/event-db-helpers.ts`:
- Around line 27-35: The code in ensureEvent uses hasTopics(event.log.topics)
then filters nulls into topics but doesn't re-check that topics[0] (topic0)
exists before persisting; add a validation after building const topics =
event.log.topics.filter(...) to ensure topics.length > 0 and throw a clear error
(or return) if empty, so that subsequent use of topics[0] (the topic0 inserted
later) cannot be undefined; update any related insertion logic that sets topic0
to use this validated topics[0].

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5a6db047-8b8c-4fe9-a04f-c7bd9c57d4fd

📥 Commits

Reviewing files that changed from the base of the PR and between 38ab526 and ffde155.

📒 Files selected for processing (42)
  • .changeset/breezy-corners-tickle.md
  • .changeset/shy-wolves-judge.md
  • .changeset/wide-chicken-dream.md
  • apps/ensapi/src/graphql-api/lib/find-domains/layers/with-ordering-metadata.ts
  • apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts
  • apps/ensapi/src/graphql-api/schema/account.ts
  • apps/ensapi/src/graphql-api/schema/domain.ts
  • apps/ensapi/src/graphql-api/schema/event.ts
  • apps/ensapi/src/graphql-api/schema/permissions.ts
  • apps/ensapi/src/graphql-api/schema/registration.ts
  • apps/ensapi/src/graphql-api/schema/resolver.ts
  • apps/ensindexer/src/lib/ensv2/event-db-helpers.ts
  • apps/ensindexer/src/lib/get-this-account-id.ts
  • apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts
  • apps/ensindexer/src/lib/ponder-helpers.ts
  • apps/ensindexer/src/lib/subgraph/db-helpers.ts
  • apps/ensindexer/src/plugins/ensv2/event-handlers.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/shared/Resolver.ts
  • apps/ensindexer/src/plugins/ensv2/plugin.ts
  • packages/datasources/src/abis/ensv2/UniversalResolverV2.ts
  • packages/datasources/src/abis/root/BaseRegistrar.ts
  • packages/datasources/src/abis/root/NameWrapper.ts
  • packages/datasources/src/abis/root/Registry.ts
  • packages/datasources/src/abis/root/UniversalResolverV1.ts
  • packages/datasources/src/abis/shared/AbstractReverseResolver.ts
  • packages/datasources/src/abis/shared/StandaloneReverseRegistrar.ts
  • packages/datasources/src/abis/shared/UniversalResolver.ts
  • packages/datasources/src/ens-test-env.ts
  • packages/datasources/src/index.ts
  • packages/datasources/src/mainnet.ts
  • packages/datasources/src/sepolia-v2.ts
  • packages/datasources/src/sepolia.ts
  • packages/ensnode-schema/src/schemas/ensv2.schema.ts
  • packages/ensnode-sdk/src/graphql-api/example-queries.ts
  • packages/integration-test-env/README.md
  • packages/integration-test-env/src/orchestrator.ts
💤 Files with no reviewable changes (1)
  • packages/datasources/src/abis/root/Registry.ts

Comment on lines +62 to +72
// Try healing based on the deployed contract's address, if exists.
//
// For these transactions, search the traces for addresses that could heal the label. All
// caller addresses are included in traces. This brute-force method is a last resort, as it
// requires an extra RPC call and parsing all addresses involved in the transaction.
// This handles contract setting their own Reverse Name in their constructor via ReverseClaimer.sol
const receipt = await context.client.getTransactionReceipt({ hash: event.transaction.hash });
if (receipt.contractAddress) {
const healedFromContractAddress = maybeHealLabelByAddrReverseSubname(
labelHash,
receipt.contractAddress,
);
if (healedFromContractAddress) return healedFromContractAddress;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unhandled error could skip trace-based fallback healing.

If getTransactionReceipt throws (network error, RPC failure), the function stops and trace-based healing (lines 82-96) is never attempted. Since trace-based healing is explicitly designed as a "last resort," it should still run even if receipt retrieval fails.

🛠️ Proposed fix: wrap receipt retrieval in try-catch
   // Try healing based on the deployed contract's address, if exists.
   //
   // This handles contract setting their own Reverse Name in their constructor via ReverseClaimer.sol
-  const receipt = await context.client.getTransactionReceipt({ hash: event.transaction.hash });
-  if (receipt.contractAddress) {
-    const healedFromContractAddress = maybeHealLabelByAddrReverseSubname(
-      labelHash,
-      receipt.contractAddress,
-    );
-    if (healedFromContractAddress) return healedFromContractAddress;
+  try {
+    const receipt = await context.client.getTransactionReceipt({ hash: event.transaction.hash });
+    if (receipt.contractAddress) {
+      const healedFromContractAddress = maybeHealLabelByAddrReverseSubname(
+        labelHash,
+        receipt.contractAddress,
+      );
+      if (healedFromContractAddress) return healedFromContractAddress;
+    }
+  } catch {
+    // Receipt retrieval failed, continue to trace-based healing
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Try healing based on the deployed contract's address, if exists.
//
// For these transactions, search the traces for addresses that could heal the label. All
// caller addresses are included in traces. This brute-force method is a last resort, as it
// requires an extra RPC call and parsing all addresses involved in the transaction.
// This handles contract setting their own Reverse Name in their constructor via ReverseClaimer.sol
const receipt = await context.client.getTransactionReceipt({ hash: event.transaction.hash });
if (receipt.contractAddress) {
const healedFromContractAddress = maybeHealLabelByAddrReverseSubname(
labelHash,
receipt.contractAddress,
);
if (healedFromContractAddress) return healedFromContractAddress;
}
// Try healing based on the deployed contract's address, if exists.
//
// This handles contract setting their own Reverse Name in their constructor via ReverseClaimer.sol
try {
const receipt = await context.client.getTransactionReceipt({ hash: event.transaction.hash });
if (receipt.contractAddress) {
const healedFromContractAddress = maybeHealLabelByAddrReverseSubname(
labelHash,
receipt.contractAddress,
);
if (healedFromContractAddress) return healedFromContractAddress;
}
} catch {
// Receipt retrieval failed, continue to trace-based healing
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts` around lines 62 -
72, Wrap the call to context.client.getTransactionReceipt in a try-catch so
failures don't abort the function; if the call throws, swallow or log the error
and continue to the existing trace-based fallback logic (the code that calls
maybeHealLabelByTxTrace / trace-based healing). Specifically, protect the block
that assigns receipt and then calls
maybeHealLabelByAddrReverseSubname(labelHash, receipt.contractAddress) so that
any thrown error only skips the contractAddress-based healing path but still
allows the trace-based healing code to run.

@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io March 10, 2026 21:39 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io March 10, 2026 21:39 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io March 10, 2026 21:39 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 54 out of 54 changed files in this pull request and generated 6 comments.


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

@shrugs shrugs marked this pull request as ready for review March 10, 2026 21:45
@shrugs shrugs requested a review from a team as a code owner March 10, 2026 21:45
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR adds comprehensive event history tracking to the ENSv2 plugin and exposes it through the GraphQL API. All ENSv2 (and relevant ENSv1) event handlers now record events into a central events table and link them to their associated entity via new join tables (domain_events, resolver_events, permissions_events). A new resolveFindEvents function provides cursor-based keyset pagination over these events with flexible where filters (topic0_in, timestamp_gte/lte, from). The Registration.start field is materialized directly on the row (removing a previously required join through the event table for ordering queries), and the ens-test-env is updated to a new devnet image with synchronized ABIs.

Key changes:

  • Event tracking: ensureDomainEvent, ensureResolverEvent, ensurePermissionsEvent helpers push events into join tables from all relevant handlers; new ENSv1Registry NewTTL/NewResolver handlers and a shared Resolver handler are added.
  • Events API: Domain.events, Resolver.events, Permissions.events, and Account.events connections added with optional where filters; Event GraphQL type gains blockNumber, transactionIndex, to, topics, data.
  • Schema expansion: events table gains blockNumber, transactionIndex, topic0, topics, data, to columns with a composite sort index; registration.start materialized.
  • Cursor consolidation: Old per-file cursor utilities replaced by a shared cursors lib using superjson for BigInt-safe encode/decode.
  • ens-test-env: Contract addresses and ABIs updated for devnet image cb8e11c, UniversalResolverV2 added.
  • Notable: A TODO in ensureEvent acknowledges null topics being observed during ABI decoding — the current null-filter approach could cause a silent NOT NULL violation if all topics for an event are null after filtering (see inline comment).

Confidence Score: 3/5

  • Safe to merge for an alpha feature; the main residual risk is the unresolved null-topics edge case in ensureEvent and the absence of integration tests for event pagination.
  • The overall design is sound and well-thought-out. The keyset pagination implementation, join-table pattern, and cursor consolidation are all correct. Score is lowered to 3 due to: (1) the acknowledged null-topic filtering in ensureEvent that could produce a NOT NULL DB error in an edge case without a guard, (2) no integration tests yet for the new events pagination/filter paths (author-acknowledged), and (3) a handful of unused imports indicating the code wasn't fully linted before submission.
  • apps/ensindexer/src/lib/ensv2/event-db-helpers.ts — null-topic guard; apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts — redundant ensureEvent calls

Important Files Changed

Filename Overview
apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts New core resolver for paginated events connections; implements cursor-based keyset pagination with optional join-table narrowing and where filters (topic0_in, timestamp_gte/lte, from). SQL composition looks correct; tuple comparison cursor is sound.
apps/ensindexer/src/lib/ensv2/event-db-helpers.ts Expands event insertion with blockNumber, transactionIndex, topic0, topics, data fields; adds ensureDomainEvent/ensureResolverEvent/ensurePermissionsEvent helpers. Has an unused makeRegistryId import and a potential runtime error if all topics are null after the null-filter step.
packages/ensnode-schema/src/schemas/ensv2.schema.ts Adds domainEvent/resolverEvent/permissionsEvents join tables, expands event table with new columns (blockNumber, transactionIndex, topic0, topics, data, to), adds composite sort index, and materializes registration.start. Has unused AccountIdString import and a truncated doc comment.
apps/ensapi/src/graphql-api/lib/cursors.ts New shared cursor encode/decode utility using base64'd superjson; replaces the old simple base64 cursors.ts in schema/ and DomainCursor's inline implementation. Consolidation is clean.
apps/ensindexer/src/plugins/ensv2/handlers/shared/Resolver.ts New shared resolver handler that records all resolver events (AddrChanged, AddressChanged, TextChanged overloads, etc.) into the resolverEvent join table via ensureResolverEvent.
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts Adds domain event tracking across all registry events and materializes registration.start. Contains redundant ensureEvent calls (called directly for eventId and again internally via ensureDomainEvent). ABI arg names updated (registeredBy→sender, changedBy→sender, resource removed).
apps/ensapi/src/graphql-api/schema/event.ts Expands Event GraphQL type with blockNumber, transactionIndex, to, topics, data fields; introduces EventsWhereInput and AccountEventsWhereInput input types for filter args on events connections.
packages/datasources/src/ens-test-env.ts Updates contract addresses and ABIs to match the new devnet image (cb8e11c); adds UniversalResolverV2 and switches UniversalResolver to use UniversalResolverV1. Helpful inline comments map devnet contract names.

Sequence Diagram

sequenceDiagram
    participant H as Event Handler<br/>(ENSv2Registry/BaseRegistrar/etc.)
    participant EDB as event-db-helpers
    participant ET as events table
    participant JT as Join Table<br/>(domain_events / resolver_events / permissions_events)
    participant API as GraphQL API<br/>(resolveFindEvents)
    participant DB as PostgreSQL

    H->>EDB: ensureDomainEvent(context, event, domainId)
    EDB->>ET: INSERT INTO events ... ON CONFLICT DO NOTHING
    ET-->>EDB: event.id
    EDB->>JT: INSERT INTO domain_events (domainId, eventId) ON CONFLICT DO NOTHING

    Note over API,DB: Query path for Domain.events / Resolver.events / Permissions.events
    API->>DB: SELECT events.* FROM events<br/>INNER JOIN domain_events ON domain_events.eventId = events.id<br/>WHERE domain_events.domainId = $1<br/>AND (timestamp, chainId, blockNumber, transactionIndex, logIndex, id) > cursor<br/>ORDER BY ... LIMIT n

    Note over API,DB: Query path for Account.events (no join table)
    API->>DB: SELECT events.* FROM events<br/>WHERE events.from = $address<br/>AND (timestamp, chainId, blockNumber, transactionIndex, logIndex, id) > cursor<br/>ORDER BY ... LIMIT n
Loading

Last reviewed commit: bee45fe

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: 4

♻️ Duplicate comments (3)
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts (2)

131-146: 🧹 Nitpick | 🔵 Trivial

Move ROOT_NODE check before computing domainId.

The ROOT_NODE check at line 142 occurs after makeENSv1DomainId(node) is called at line 139. This is inconsistent with handleTransfer (lines 109-110) which checks ROOT_NODE first. While not functionally broken (the function still returns early), the ordering is misleading and wastes a function call.

♻️ Proposed fix
   async function handleNewTTL({
     context,
     event,
   }: {
     context: Context;
     event: EventWithArgs<{ node: Node }>;
   }) {
     const { node } = event.args;
-    const domainId = makeENSv1DomainId(node);
 
     // ENSv2 model does not include root node, no-op
     if (node === ROOT_NODE) return;
 
+    const domainId = makeENSv1DomainId(node);
+
     // push event to domain history
     await ensureDomainEvent(context, event, domainId);
   }
🤖 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 131 - 146, Move the ROOT_NODE check in handleNewTTL ahead of calling
makeENSv1DomainId: first return if node === ROOT_NODE, then compute domainId
with makeENSv1DomainId(node) and call ensureDomainEvent(context, event,
domainId). This mirrors handleTransfer and avoids an unnecessary call to
makeENSv1DomainId when the node is the root.

148-166: 🧹 Nitpick | 🔵 Trivial

Move ROOT_NODE check before computing domainId.

Same issue as handleNewTTL - the ROOT_NODE check at line 159 should occur before makeENSv1DomainId(node) at line 156 for consistency with other handlers.

♻️ Proposed fix
   async function handleNewResolver({
     context,
     event,
   }: {
     context: Context;
     event: EventWithArgs<{ node: Node }>;
   }) {
     const { node } = event.args;
-    const domainId = makeENSv1DomainId(node);
 
     // ENSv2 model does not include root node, no-op
     if (node === ROOT_NODE) return;
 
+    const domainId = makeENSv1DomainId(node);
+
     // NOTE: Domain-Resolver relations are handled by the protocol-acceleration plugin and are not
     // directly indexed here
 
     // push event to domain history
     await ensureDomainEvent(context, event, domainId);
   }
🤖 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 148 - 166, In handleNewResolver, move the ROOT_NODE guard before calling
makeENSv1DomainId(node): check "if (node === ROOT_NODE) return;" immediately
after extracting node from event.args and only call makeENSv1DomainId(node) when
node is not the root; this keeps behavior consistent with other handlers (e.g.,
handleNewTTL) and avoids unnecessary domainId computation before the early
return; ensure references to makeENSv1DomainId, ROOT_NODE, handleNewResolver,
and ensureDomainEvent remain intact.
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts (1)

279-280: 🧹 Nitpick | 🔵 Trivial

Consider moving ensureDomainEvent after registration updates.

The ensureDomainEvent call at lines 279-280 is placed before the registration update logic (lines 282-294), which is inconsistent with other handlers in this file where events are logged after state mutations. While not functionally incorrect, moving it after line 294 would align with the established pattern.

♻️ Proposed fix
       if (!registration) {
         throw new Error(`Invariant(NameWrapper:NameUnwrapped): Registration expected`);
       }
 
-      // push event to domain history
-      await ensureDomainEvent(context, event, domainId);
-
       if (registration.type === "BaseRegistrar") {
         // if this is a wrapped BaseRegisrar Registration, unwrap it
         await context.db.update(schema.registration, { id: registration.id }).set({
           wrapped: false,
           fuses: null,
           // expiry: null // TODO: NameWrapper expiry logic? maybe nothing to do here
         });
       } else {
         // otherwise, deactivate the latest registration by setting its expiry to this block
         await context.db.update(schema.registration, { id: registration.id }).set({
           expiry: event.block.timestamp,
         });
       }
 
+      // push event to domain history
+      await ensureDomainEvent(context, event, domainId);
+
       // NOTE: we don't need to adjust Domain.ownerId because NameWrapper always calls ens.setOwner
     },
   );
🤖 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/NameWrapper.ts` around lines
279 - 280, The call to ensureDomainEvent(context, event, domainId) is placed
before the registration update block, which breaks the established pattern of
logging events after state mutations; move the ensureDomainEvent call so it
executes after the registration update logic (the block that mutates
registration state following the event—i.e., the code between ensureDomainEvent
and line 294), keeping the same arguments (context, event, domainId) and
preserving await semantics so the event is recorded only after the registration
updates complete; this aligns NameWrapper handler ordering with the other
handlers in the file.
🤖 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/ensapi/src/graphql-api/schema/account.integration.test.ts`:
- Around line 102-103: The two empty test suites "Account.events" and
"Account.events pagination" add no assertions; add at least one end-to-end test
in each describe block that executes the GraphQL Account.events query (via the
existing test client setup used in account.integration.test.ts), asserts the
returned event items match expected fixtures (IDs/types/timestamps), verifies
cursor ordering/continuity (ascending/descending as required), and in the
pagination suite call the query with a limit/cursor to assert page boundaries
and nextCursor behavior using expect assertions; place tests inside the existing
describe blocks so they fail if event data, ordering, or cursor logic in
Account.events changes.
- Around line 89-100: The commented EVENT_SORT_COLUMNS block documents the
stable ordering for events but will drift if left as a comment; either remove it
or make it executable by declaring the constant (EVENT_SORT_COLUMNS =
[schema.event.timestamp, schema.event.chainId, schema.event.blockNumber,
schema.event.transactionIndex, schema.event.logIndex, schema.event.id] as const)
and then use it in the event pagination tests (e.g., introduce a helper like
assertStableEventSortOrder(events, EVENT_SORT_COLUMNS) or incorporate it into
existing pagination assertions) so the contract is enforced by tests rather than
only documented in comments.

In `@packages/ensnode-sdk/src/graphql-api/example-queries.ts`:
- Around line 142-160: The example GraphQL query DomainEvents currently uses the
bare events connection; update it to demonstrate cursor pagination and filters
by adding variables (e.g. $first: Int, $after: String, $where: EventFilter) to
the query signature and passing them in the variables object, then call events
with arguments like events(first: $first, after: $after, where: $where) so the
playground shows both pagination and a where filter (e.g. filter by from,
topics, or timestamp); adjust the variables.default to include sensible defaults
for name plus first/after/where to exercise the new API surface.
- Around line 344-349: The SepoliaV2 resolver lookup currently calls
getDatasourceContract(ENSNamespaceIds.SepoliaV2,
DatasourceNames.ReverseResolverRoot, "DefaultPublicResolver5") which throws at
module import if that datasource is absent; change this to a non-throwing lookup
by calling maybeGetDatasourceContract(...) (near the other top-level datasource
constants) to get an optional value (e.g. sepoliaDefaultPublicResolver5) and
then only include the ENSNamespaceIds.SepoliaV2 entry in the resolver overrides
object when that optional value is defined so the examples module no longer
fails during evaluation.

---

Duplicate comments:
In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts`:
- Around line 131-146: Move the ROOT_NODE check in handleNewTTL ahead of calling
makeENSv1DomainId: first return if node === ROOT_NODE, then compute domainId
with makeENSv1DomainId(node) and call ensureDomainEvent(context, event,
domainId). This mirrors handleTransfer and avoids an unnecessary call to
makeENSv1DomainId when the node is the root.
- Around line 148-166: In handleNewResolver, move the ROOT_NODE guard before
calling makeENSv1DomainId(node): check "if (node === ROOT_NODE) return;"
immediately after extracting node from event.args and only call
makeENSv1DomainId(node) when node is not the root; this keeps behavior
consistent with other handlers (e.g., handleNewTTL) and avoids unnecessary
domainId computation before the early return; ensure references to
makeENSv1DomainId, ROOT_NODE, handleNewResolver, and ensureDomainEvent remain
intact.

In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts`:
- Around line 279-280: The call to ensureDomainEvent(context, event, domainId)
is placed before the registration update block, which breaks the established
pattern of logging events after state mutations; move the ensureDomainEvent call
so it executes after the registration update logic (the block that mutates
registration state following the event—i.e., the code between ensureDomainEvent
and line 294), keeping the same arguments (context, event, domainId) and
preserving await semantics so the event is recorded only after the registration
updates complete; this aligns NameWrapper handler ordering with the other
handlers in the file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3852ba04-f47f-46bf-96f4-8c792a0a6178

📥 Commits

Reviewing files that changed from the base of the PR and between ffde155 and 11ede60.

📒 Files selected for processing (39)
  • apps/ensapi/src/graphql-api/lib/cursors.ts
  • apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts
  • apps/ensapi/src/graphql-api/schema/account.integration.test.ts
  • apps/ensapi/src/graphql-api/schema/permissions.ts
  • apps/ensindexer/src/lib/ensv2/event-db-helpers.ts
  • apps/ensindexer/src/lib/heal-addr-reverse-subname-label.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts
  • packages/datasources/src/abis/basenames/BaseRegistrar.ts
  • packages/datasources/src/abis/basenames/EARegistrarController.ts
  • packages/datasources/src/abis/basenames/L1Resolver.ts
  • packages/datasources/src/abis/basenames/RegistrarController.ts
  • packages/datasources/src/abis/basenames/Registry.ts
  • packages/datasources/src/abis/basenames/ReverseRegistrar.ts
  • packages/datasources/src/abis/basenames/UpgradeableRegistrarController.ts
  • packages/datasources/src/abis/ensv2/ETHRegistrar.ts
  • packages/datasources/src/abis/ensv2/EnhancedAccessControl.ts
  • packages/datasources/src/abis/ensv2/Registry.ts
  • packages/datasources/src/abis/ensv2/UniversalResolverV2.ts
  • packages/datasources/src/abis/lineanames/BaseRegistrar.ts
  • packages/datasources/src/abis/lineanames/EthRegistrarController.ts
  • packages/datasources/src/abis/lineanames/NameWrapper.ts
  • packages/datasources/src/abis/lineanames/Registry.ts
  • packages/datasources/src/abis/root/BaseRegistrar.ts
  • packages/datasources/src/abis/root/LegacyEthRegistrarController.ts
  • packages/datasources/src/abis/root/NameWrapper.ts
  • packages/datasources/src/abis/root/Registry.ts
  • packages/datasources/src/abis/root/UniversalRegistrarRenewalWithReferrer.ts
  • packages/datasources/src/abis/root/UniversalResolverV1.ts
  • packages/datasources/src/abis/root/UnwrappedEthRegistrarController.ts
  • packages/datasources/src/abis/root/WrappedEthRegistrarController.ts
  • packages/datasources/src/abis/seaport/Seaport1.5.ts
  • packages/datasources/src/abis/shared/AbstractReverseResolver.ts
  • packages/datasources/src/abis/shared/LegacyPublicResolver.ts
  • packages/datasources/src/abis/shared/Resolver.ts
  • packages/datasources/src/abis/shared/StandaloneReverseRegistrar.ts
  • packages/datasources/src/abis/threedns/ThreeDNSToken.ts
  • packages/ensnode-schema/src/schemas/ensv2.schema.ts
  • packages/ensnode-sdk/src/graphql-api/example-queries.ts

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: Domain/Resolver/Registration History

2 participants