Skip to content

feat(ensindex): introduce registrars plugin#1249

Merged
tk-o merged 17 commits intomainfrom
feat/1228-registrars-plugin
Nov 6, 2025
Merged

feat(ensindex): introduce registrars plugin#1249
tk-o merged 17 commits intomainfrom
feat/1228-registrars-plugin

Conversation

@tk-o
Copy link
Copy Markdown
Contributor

@tk-o tk-o commented Nov 4, 2025

Related to:

Replaces:

Resolves:


Before sharing what's been done in this PR, I'll share what's not:

  • The registrars plugin does not index FQDNs related to registrations
    • The simple idea I have is to make the ENSApi HTTP route handler an integration point for data coming from subgraph plugin and subregistry plugin. It requires ENSIndexer to run with both plugins in order to provide data required to serve the Registrar Actions API route. This route, once added in a follow-up PR, will serve data about registartions and renewal, including FQDN for each registration.

Now, about what's been achieved...

This PR introduces the registrars plugin.

The main goal of the new plugin is to index activity in all subregistries which have ever managed names for:

  • Ethnames
  • Basenames
  • Lineanames

We now track activity in BaseRegistrar implementation contracts with following event handlers:

  • NameRegistered and NameRenewed (and their equivalents) to know about all registration updates within the subregistry.

Furthermore, we track "Registrar Actions", which are NameRegistered and NameRenewed events emitted by registrar controllers.

Why do we need to track NameRegistered and NameRenewed events emitted by different types of contracts?

I'd be best to only index events from registrar controller contracts. Those events can include useful information, ie. about fees, or referrals.
However, we don't currently index all registrar controllers contracts for various reasons. For example, there were couple of special use case registrar controllers that were used for addressing security incident. We have to still learn how to best index those.

And that's where NameRegistered and NameRenewed events emitted by BaseRegistrar implementation contracts are useful. We index all those contracts that ever existed, and thanks to that, we get a comprehensive view on all registrations that were ever created.

One major todo item is to introduce an invariant such that we only index renewals that were made for either:

  1. Active registration (now < expiresAt).
  2. Inactive registration, but within the grace period (expiresAt < now < expiresAt + GRACE_PERIOD).

We cannot implement this invariant at the moment, as it requires all registrar controllers to be indexed first.

TODOs

Review

Feel free to start review in the following order 👇

ENSNode Schema

packages/ensnode-schema/src/schemas/registrars.schema.ts includes new database schemas:

  • subregistries tracks all indexed subregistries.
  • registrationLifecycles tracks an aggregate statte of all registrations lifecacles that were ever created in relevant BaseRegistrar implementation contract, and updated by relevant Registrar Controller contracts.
  • registrarAction tracks "logical" actions taken by the end user, such as "registration" action, and "renewal" action. Includes an ID reference to a relevant onchain event, in case onchain event details are required.
  • tempLogicalSubregistryAction which is an internal-only table which helps connecting all state transitions spread across multiple onchain events into a single "logical" registrar action record.

ENS Referrals

The @namehash/ens-referrals package has become a dependency for:

  • @ensnode/ensnode-sdk package (which also re-exports some types and functions for the benefit of clients, i.e. ensindexer app).

ENSNode SDK

The main update is the new registrar-actions module at packages/ensnode-sdk/src/registrar-actions.

ENSIndexer

The registrars plugin has been created and splits into two parts:

  • modules focused around owned names
    • apps/ensindexer/src/plugins/registrars/ethnames
    • apps/ensindexer/src/plugins/registrars/basenames
    • apps/ensindexer/src/plugins/registrars/lineanames
  • shared modules
    • apps/ensindexer/src/plugins/registrars/shared
      • interacts with index database

Modules include:

  • handlers with event handlers defined for indexed:
    • BaseRegistrar implementation contracts
    • RegistrarController contracts
  • lib with module-specific helpers

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Nov 4, 2025

🦋 Changeset detected

Latest commit: 0b4b32b

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

This PR includes changesets to release 14 packages
Name Type
@ensnode/ensnode-schema Minor
@ensnode/ensnode-sdk Minor
ensindexer Minor
ensadmin Minor
ensapi Minor
ensrainbow Minor
@ensnode/ensnode-react Minor
@ensnode/ensrainbow-sdk Minor
@ensnode/datasources Minor
@ensnode/ponder-metadata Minor
@ensnode/ponder-subgraph Minor
@ensnode/shared-configs Minor
@docs/ensnode Minor
@docs/ensrainbow Minor

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

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

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Nov 4, 2025

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

Project Deployment Preview Comments Updated (UTC)
admin.ensnode.io Ready Ready Preview Comment Nov 6, 2025 6:27pm
ensnode.io Ready Ready Preview Comment Nov 6, 2025 6:27pm
ensrainbow.io Ready Ready Preview Comment Nov 6, 2025 6:27pm

Copy link
Copy Markdown
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

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

@tk-o Hey thanks for sharing this! Overall looking good. Shared a detail review and feedback mostly on terminology but also a few ideas that would influence code.

subregistryId: t.text().primaryKey(),

/**
* The node of a name the subregistry manager. Example managed names:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* The node of a name the subregistry manager. Example managed names:
* The node (namehash) of the name the subregistry manages subnames of. Example subregistry managed names:

Comment on lines +37 to +39
/**
* Registration Lifecycle
*/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
/**
* Registration Lifecycle
*/
/**
* Registration Lifecycle
*
* A "registration lifecycle" represents a single cycle of a name being registered once followed by renewals
* (expiry date extensions) any number of times.
*
* Note that this data model only tracks the *most recently created* "registration lifecycle" record for a
* name and doesn't track *all* "registration lifecycle" records for a name across time. Therefore, if a
* name goes through multiple cycles of:
* (registration -> expiry -> release) -> (registration -> expiry -> release) -> etc..
* this data model only stores data of the most recently created "registration lifecycle".
*
* For now we make the following simplifying assumptions:
* 1. That no two subregistries hold state for the same node.
* 2. That the subregistry associated with the name X in the ENS root registry exclusively holds state for subnames of X.
*
* These simplifying assumptions happen to be true for the scope of our current indexing logic,
* but nothing in the ENS protocol fundamentally forces this to always be true. Therefore this data model
* will need refactoring in the future as our indexing logic expands to handle more complex scenarios.
*/

Comment on lines +44 to +48
* The node of the FQDN of the domain this is associated with,
* guaranteed to be a subname of the associated subregistry
* for which the registration was executed.
*
* Guaranteed to be a hex string representation of 32-bytes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* The node of the FQDN of the domain this is associated with,
* guaranteed to be a subname of the associated subregistry
* for which the registration was executed.
*
* Guaranteed to be a hex string representation of 32-bytes.
* The node (namehash) of the FQDN of the domain the registration lifecycle is associated with.
*
* Guaranteed to be a subname of the node (namehash) of the subregistry identified by `subregistryId`.
*
* Guaranteed to be a hex string representation of 32-bytes.

Comment on lines +289 to +301
* Logical Subregistry Action Metadata
*
* Building a single Logical Subregistry Action requires data from multiple
* onchain events. While handling the first event, we create a temporary
* Logical Subregistry Action Metadata record where we store `logicalEventId`.
*
* The `logicalEventId` is used by subsequent event handlers to update
* the Logical Subregistry Action record. In order to get `logicalEventId`,
* an event handler creates `logicalEventKey` from the currently handled
* onchain event.
*
* The very last event handler must remove the record referenced with
* `logicalEventKey` value.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* Logical Subregistry Action Metadata
*
* Building a single Logical Subregistry Action requires data from multiple
* onchain events. While handling the first event, we create a temporary
* Logical Subregistry Action Metadata record where we store `logicalEventId`.
*
* The `logicalEventId` is used by subsequent event handlers to update
* the Logical Subregistry Action record. In order to get `logicalEventId`,
* an event handler creates `logicalEventKey` from the currently handled
* onchain event.
*
* The very last event handler must remove the record referenced with
* `logicalEventKey` value.
* Logical Subregistry Action Metadata
*
* NOTE: This table is an internal implementation detail of ENSIndexer and should not be queried outside of
* ENSIndexer.
*
* Building a "logical subregistry action" record may require data from multiple onchain events. To help
* aggregate data from multiple events into a single "logical subregistry action" ENSIndexer may temporarily
* store data here to achieve this data aggregation.
*
* Note how multiple "logical subregistry actions" may be taken on the same `node` in the same
* `transactionHash`. For example, consider a case of a single transaction registering a name and
* subsequently renewing it twice. While this may be silly it is technically possible and therefore such cases
* must be considered. To support such cases, when the last event handler for a "logical subregistry action"
* has completed its processing the record referenced by the `logicalEventKey` must be removed.

Comment on lines +314 to +317
* Logical Event ID
*
* A string holding the ID value to an existing Logical Registrar Action
* record that was inserted while e use this event to initiate the Logical Registrar Action record.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* Logical Event ID
*
* A string holding the ID value to an existing Logical Registrar Action
* record that was inserted while e use this event to initiate the Logical Registrar Action record.
* Logical Event ID
*
* A string holding the `id` value of the existing "logical registrar action" record that is currently being
* built as an aggregation of onchain events.
*
* May be used by subsequent event handlers to identify which "logical registrar action" to aggregate
* additional indexed state into.

Comment on lines +327 to +328
*
* - many RegistrationLifecycles
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
*
* - many RegistrationLifecycles
*
* Each Subregistry is related to:
* - 0 or more RegistrationLifecycles

Comment on lines +336 to +337
*
* - exactly one Subregistry
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
*
* - exactly one Subregistry
*
* Each Registration Lifecycle is related to:
* - exactly one Subregistry

Comment on lines +350 to +351
*
* - exactly one Registration Lifecycle
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
*
* - exactly one Registration Lifecycle
*
* Each Registrar Action is related to:
* - exactly one Registration Lifecycle (note the docs on Registration Lifecycle explaining how these records may be
* recycled across time).

Copy link
Copy Markdown
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

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

@tk-o Hey thanks for sharing this! Overall looking good. Shared a detail review and feedback mostly on terminology but also a few ideas that would influence code.

tk-o added 5 commits November 5, 2025 16:17
Drop "logical" from symbol names, keep the "logical" reference in JSDocs."
Better type inference, support for bigint to number conversion.
The module includestypes and helpers supporting goals of the `registrars` plugin.
tk-o added 4 commits November 6, 2025 07:14
Hosts functionality for working with dates and time.
Favour simple business logic functions. Convert types, if possible, at the edge (ponder event handlers).
@tk-o tk-o changed the title WIP feat(ensindex): introduce registrars plugin feat(ensindex): introduce registrars plugin Nov 6, 2025
@tk-o tk-o marked this pull request as ready for review November 6, 2025 08:32
@tk-o tk-o requested a review from a team as a code owner November 6, 2025 08:32
Copy link
Copy Markdown
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

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

@tk-o Hey great to see these updates! I need to step away for 20 minutes but here's some quick feedback that was focused on a few other little refinements to the schema.

@@ -0,0 +1,358 @@
/**
* Schema Definitions for tracking of ENS subregistries.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@tk-o I think best to leave the plugin name as it is for now, but just update this comment.

Suggested change
* Schema Definitions for tracking of ENS subregistries.
* Schema Definitions for tracking of ENS registrars.

* Identifies the chainId and address of the smart contract associated
* with the subregistry.
*
* Guaranteed to be a string formatted according to the CAIP-10 standard.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* Guaranteed to be a string formatted according to the CAIP-10 standard.
* Guaranteed to be a fully lowercase string formatted according to the CAIP-10 standard.

Is that fair?

* - `base.eth`
* - `linea.eth`
*
* Guaranteed to be a hex string representation of 32-bytes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* Guaranteed to be a hex string representation of 32-bytes.
* Guaranteed to be a fully lowercase hex string representation of 32-bytes.

Is that fair?

* Guaranteed to be a subname of the node (namehash) of the subregistry
* identified by `subregistryId`.
*
* Guaranteed to be a hex string representation of 32-bytes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* Guaranteed to be a hex string representation of 32-bytes.
* Guaranteed to be a fully lowercase hex string representation of 32-bytes.

* Identifies the chainId and address of the subregistry smart contract
* that manages the registration lifecycle.
*
* Guaranteed to be a string formatted according to the CAIP-10 standard.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* Guaranteed to be a string formatted according to the CAIP-10 standard.
* Guaranteed to be a fully lowercase string formatted according to the CAIP-10 standard.

Comment on lines +360 to +361
* - Ordered chronologically (ascending) by logIndex within `blockNumber`
* on `chainId`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* - Ordered chronologically (ascending) by logIndex within `blockNumber`
* on `chainId`.
* - Ordered chronologically (ascending) by logIndex within `blockNumber`.

Comment on lines +362 to +363
* - The first element in the array is equal to the `id` of
* the "logical registrar action".
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* - The first element in the array is equal to the `id` of
* the "logical registrar action".
* - The first element in the array is equal to the `id` of
* the overall "logical registrar action" record.

Comment on lines +373 to +376
*
* Guaranteed to:
* - Reference at least one event.
* - Keep event references ordered chronologically, by event log index.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
*
* Guaranteed to:
* - Reference at least one event.
* - Keep event references ordered chronologically, by event log index.

These ideas are already documented above.

* NOTE: This table is an internal implementation detail of ENSIndexer and
* should not be queried outside of ENSIndexer.
*
* Building a "logical subregistry action" record may require data from
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I note that we are mixing up the following terminology:

  1. "logical subregistry action"
  2. "logical registrar action"
  3. "Logical" Registrar Action ID

Suggest we update all of these to one of the following depending on the context:

  1. "logical registrar action"
  2. "logical registrar action" ID

Please search through all our code and docs to refine these inconsistencies. Thanks!

/**
* Logical Event Key
*
* A string formatted as:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* A string formatted as:
* A fully lowercase string formatted as:

Copy link
Copy Markdown
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

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

@tk-o Hey great job 🚀 Reviewed and shared feedback. Please take the lead to merge this when ready 👍

- direct subnames of the Lineanames registrar managed name (ex: for mainnet `linea.eth` but varies for other namespaces).

Additionally indexes:
- All Registrar Controllers ever associated with a known Registrar contract.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we remove this line now?

import { durationBetween } from "./datetime";

describe("datetime", () => {
describe("durationBetween()", () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggest to also add a unit test for the duration between being 0

import { bigIntToNumber } from "./numbers";

describe("Numbers", () => {
it("can convert bigint to number when possible", () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you please create another unit test demonstrating successful conversion of the smallest possible successful conversion?

Comment on lines +4 to +5
* @throws when value is too low.
* @throws when value is too high .
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* @throws when value is too low.
* @throws when value is too high .
* @throws when value is outside the range of `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER`.

*/
export interface RegistrationLifecycle {
/**
* Subregistry account that this Registration Lifecycle belongs to.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* Subregistry account that this Registration Lifecycle belongs to.
* Subregistry that manages this Registration Lifecycle.

/**
* Block ref
*
* References the block where "logical" Registrar Action was executed.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* References the block where "logical" Registrar Action was executed.
* References the block where the "logical" Registrar Action was executed.

pricing: RegistrarActionPricing;

/**
* Referral information related to performing this "logical" Registrar Action.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* Referral information related to performing this "logical" Registrar Action.
* Referral information associated with this "logical" Registrar Action.

registrationLifecycle: RegistrationLifecycle;

/**
* Pricing information for performing this "logical" Registrar Action.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* Pricing information for performing this "logical" Registrar Action.
* Pricing information associated with this "logical" Registrar Action.

Comment on lines +226 to +227
* Registration Lifecycle that this "logical" Registrar Action was
* executed for.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* Registration Lifecycle that this "logical" Registrar Action was
* executed for.
* Registration Lifecycle associated with this "logical" Registrar Action.

* Account that initiated the registrarAction and is paying the "total".
* It may not be the owner of the name:
*
* 1. When a name is registered, the initial owner of the name may be
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's some nice ideas documented here. Can you please confirm they are also documented in the Drizzle schema?

tk-o added 2 commits November 6, 2025 18:33
…l registrations and renewals for direct subnames of `eth`, `base.eth`, and `linea.eth`.
Rename `RegistrarActionPricingNotApplicable` type to be `RegistrarActionPricingUnknown`
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.

2 participants