Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
🦋 Changeset detectedLatest commit: bee45fe The changes in this PR will be included in the next version bump. This PR includes changesets to release 19 packages
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 |
📝 WalkthroughWalkthroughThis 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
Sequence DiagramssequenceDiagram
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
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}}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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
eventstable 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
Domainhistory. - Added GraphQL
*.eventsconnections 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 (rolesBitmap → roleBitmap). |
| 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (19)
apps/ensapi/src/graphql-api/lib/connection-helpers.tsapps/ensapi/src/graphql-api/lib/cursors.tsapps/ensapi/src/graphql-api/lib/find-domains/domain-cursor.test.tsapps/ensapi/src/graphql-api/lib/find-domains/domain-cursor.tsapps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.tsapps/ensapi/src/graphql-api/lib/find-events/event-cursor.tsapps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.tsapps/ensapi/src/graphql-api/schema/constants.tsapps/ensapi/src/graphql-api/schema/cursors.tsapps/ensapi/src/graphql-api/schema/domain.tsapps/ensapi/src/graphql-api/schema/event.tsapps/ensapi/src/graphql-api/schema/registry.tsapps/ensapi/src/graphql-api/schema/resolver.tsapps/ensindexer/src/lib/ensv2/event-db-helpers.tsapps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.tspackages/datasources/src/abis/ensv2/ETHRegistrar.tspackages/datasources/src/abis/ensv2/EnhancedAccessControl.tspackages/datasources/src/abis/ensv2/Registry.tspackages/ensnode-schema/src/schemas/ensv2.schema.ts
💤 Files with no reviewable changes (1)
- apps/ensapi/src/graphql-api/schema/cursors.ts
apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts
Outdated
Show resolved
Hide resolved
…e integration tests, etc
…ilable, indicating the start timestamp of the Registration.
…he set of Events for which this Account is the sender (i.e. `Transaction.from`).
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
Dismissed
Show dismissed
Hide dismissed
apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 10
♻️ Duplicate comments (2)
apps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.ts (1)
54-56:⚠️ Potential issue | 🟠 MajorCast bigint cursor values in
eventCursorWhere.The tuple comparison is still interpolating bigint-backed cursor fields without explicit
::bigintcasts. That can breakbefore/afterpagination 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 | 🔴 CriticalValidate
topic0after filtering outnulltopics.The precondition only checks that the original array is non-empty. If
event.log.topicsis[null], Line 35 turns it into[]and Line 59 still insertstopic0: topics[0]. That can fail the insert or persist a malformedeventrow.🔧 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
📒 Files selected for processing (42)
.changeset/breezy-corners-tickle.md.changeset/shy-wolves-judge.md.changeset/wide-chicken-dream.mdapps/ensapi/src/graphql-api/lib/find-domains/layers/with-ordering-metadata.tsapps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.tsapps/ensapi/src/graphql-api/schema/account.tsapps/ensapi/src/graphql-api/schema/domain.tsapps/ensapi/src/graphql-api/schema/event.tsapps/ensapi/src/graphql-api/schema/permissions.tsapps/ensapi/src/graphql-api/schema/registration.tsapps/ensapi/src/graphql-api/schema/resolver.tsapps/ensindexer/src/lib/ensv2/event-db-helpers.tsapps/ensindexer/src/lib/get-this-account-id.tsapps/ensindexer/src/lib/heal-addr-reverse-subname-label.tsapps/ensindexer/src/lib/ponder-helpers.tsapps/ensindexer/src/lib/subgraph/db-helpers.tsapps/ensindexer/src/plugins/ensv2/event-handlers.tsapps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.tsapps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.tsapps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.tsapps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.tsapps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.tsapps/ensindexer/src/plugins/ensv2/handlers/ensv2/EnhancedAccessControl.tsapps/ensindexer/src/plugins/ensv2/handlers/shared/Resolver.tsapps/ensindexer/src/plugins/ensv2/plugin.tspackages/datasources/src/abis/ensv2/UniversalResolverV2.tspackages/datasources/src/abis/root/BaseRegistrar.tspackages/datasources/src/abis/root/NameWrapper.tspackages/datasources/src/abis/root/Registry.tspackages/datasources/src/abis/root/UniversalResolverV1.tspackages/datasources/src/abis/shared/AbstractReverseResolver.tspackages/datasources/src/abis/shared/StandaloneReverseRegistrar.tspackages/datasources/src/abis/shared/UniversalResolver.tspackages/datasources/src/ens-test-env.tspackages/datasources/src/index.tspackages/datasources/src/mainnet.tspackages/datasources/src/sepolia-v2.tspackages/datasources/src/sepolia.tspackages/ensnode-schema/src/schemas/ensv2.schema.tspackages/ensnode-sdk/src/graphql-api/example-queries.tspackages/integration-test-env/README.mdpackages/integration-test-env/src/orchestrator.ts
💤 Files with no reviewable changes (1)
- packages/datasources/src/abis/root/Registry.ts
| // 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; | ||
| } |
There was a problem hiding this comment.
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.
| // 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.
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
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.
Greptile SummaryThis 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 Key changes:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
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
Last reviewed commit: bee45fe |
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts (2)
131-146: 🧹 Nitpick | 🔵 TrivialMove ROOT_NODE check before computing domainId.
The
ROOT_NODEcheck at line 142 occurs aftermakeENSv1DomainId(node)is called at line 139. This is inconsistent withhandleTransfer(lines 109-110) which checksROOT_NODEfirst. 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 | 🔵 TrivialMove ROOT_NODE check before computing domainId.
Same issue as
handleNewTTL- theROOT_NODEcheck at line 159 should occur beforemakeENSv1DomainId(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 | 🔵 TrivialConsider moving
ensureDomainEventafter registration updates.The
ensureDomainEventcall 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
📒 Files selected for processing (39)
apps/ensapi/src/graphql-api/lib/cursors.tsapps/ensapi/src/graphql-api/lib/find-events/find-events-resolver.tsapps/ensapi/src/graphql-api/schema/account.integration.test.tsapps/ensapi/src/graphql-api/schema/permissions.tsapps/ensindexer/src/lib/ensv2/event-db-helpers.tsapps/ensindexer/src/lib/heal-addr-reverse-subname-label.tsapps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.tsapps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.tspackages/datasources/src/abis/basenames/BaseRegistrar.tspackages/datasources/src/abis/basenames/EARegistrarController.tspackages/datasources/src/abis/basenames/L1Resolver.tspackages/datasources/src/abis/basenames/RegistrarController.tspackages/datasources/src/abis/basenames/Registry.tspackages/datasources/src/abis/basenames/ReverseRegistrar.tspackages/datasources/src/abis/basenames/UpgradeableRegistrarController.tspackages/datasources/src/abis/ensv2/ETHRegistrar.tspackages/datasources/src/abis/ensv2/EnhancedAccessControl.tspackages/datasources/src/abis/ensv2/Registry.tspackages/datasources/src/abis/ensv2/UniversalResolverV2.tspackages/datasources/src/abis/lineanames/BaseRegistrar.tspackages/datasources/src/abis/lineanames/EthRegistrarController.tspackages/datasources/src/abis/lineanames/NameWrapper.tspackages/datasources/src/abis/lineanames/Registry.tspackages/datasources/src/abis/root/BaseRegistrar.tspackages/datasources/src/abis/root/LegacyEthRegistrarController.tspackages/datasources/src/abis/root/NameWrapper.tspackages/datasources/src/abis/root/Registry.tspackages/datasources/src/abis/root/UniversalRegistrarRenewalWithReferrer.tspackages/datasources/src/abis/root/UniversalResolverV1.tspackages/datasources/src/abis/root/UnwrappedEthRegistrarController.tspackages/datasources/src/abis/root/WrappedEthRegistrarController.tspackages/datasources/src/abis/seaport/Seaport1.5.tspackages/datasources/src/abis/shared/AbstractReverseResolver.tspackages/datasources/src/abis/shared/LegacyPublicResolver.tspackages/datasources/src/abis/shared/Resolver.tspackages/datasources/src/abis/shared/StandaloneReverseRegistrar.tspackages/datasources/src/abis/threedns/ThreeDNSToken.tspackages/ensnode-schema/src/schemas/ensv2.schema.tspackages/ensnode-sdk/src/graphql-api/example-queries.ts
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:
ensureDomainEvent,ensureResolverEvent,ensurePermissionsEventnow push events to the appropriate join tablestopic0_in,timestamp_gte/lte,fromfilters. thethrough: { table, scope }pattern for narrowing via join tablesProblem & Motivation
Registration.startwas previously removed from the GraphQL API in favor of joining throughregistration.event.timestamp, making queries slower and more complexWhat Changed (Concrete)
Registration.startmaterialized — stored on the registration row, exposed in GraphQL, used directly for ordering (removes event table join from with-ordering-metadata).Account.eventsfield added. where filters (topic0_in, timestamp_gte/lte, from) on all *.events connections. resolveFindEvents queries event table directly, optionally narrowed via join table.Design & Planning
resolveFindEventsdesign 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.wherefilter design follows the find-domains pattern (input types → internal filter shape → SQL conditions)Self-Review
with-ordering-metadataafter materializingregistration.startthrough: { table, scope }reads clearly at callsitescursors.tsin schema/ replaced by sharedcursorslibDownstream & Consumer Impact
Registration.startfield added (non-breaking, new field)Account.eventsconnection added (non-breaking, new field)Domain.events,Resolver.events,Permissions.eventsnow acceptwherearg (non-breaking, optional)Eventtype gainsblockNumber,transactionIndex,to,topics,datafields (non-breaking)EventsWhereInput,AccountEventsWhereInputTesting Evidence
resolveFindEventsRisk Analysis
Pre-Review Checklist (Blocking)