Understanding ENSNode V1
What is ENSNode
ENSNode is the new multichain indexer for ENS and ENSv2. It is built on Ponder and provides enhanced capabilities over the ENS Subgraph, as well as being more efficient, flexible, and maintainable. ENSNode’s enhanced capabilities include multichain and multiregistrar indexing while maintaining backwards compatibility with existing ENS Subgraph APIs.
ENSNode Version 1
ENSNode version 1 (V1
), discussed here, prioritizes equivalency with the ENS Subgraph, which drove many architectural decisions.
Core Design Principles
-
Full Backwards Compatibility w/ ENS Subgraph
- Full support for existing ENS Subgraph queries used by
ensjs
andens-app-v3
- Drop-in replacement for applications using the ENS Subgraph
- Verified compatibility through extensive testing with
ens-test-env
and ens-subgraph-transition-tools (see Subgraph Compatibility)
- Full support for existing ENS Subgraph queries used by
-
Multi-Registry Plugin Architecture
- Support for indexing ENS data across multiple chains & subregistries (i.e. mainnet, Base, Linea)
- Plugins can be activated independently or in combination
-
Built on Ponder
- Improved indexing speed (>10x faster than ENS Subgraph)
- Isolated indexing schemas (supporting branches, staging environments)
- Access your indexed data directly from Postgres
@ponder/client
for efficent client-side live queries
-
Self-hostable Decentralization Approach
- Self-hostable infrastructure
- Bring-your-own Postgres
- Bring-your-own ENSRainbow
Plugins
ENSIndexer implements the core subgraph indexing logic within shared handler functions that mirror their subgraph counterpart. These shared handlers functions are in apps/ensindexer/src/handlers/*.ts
. The eth
plugin then implements subgraph-compatible logic 1:1, primarily relying on the shared handlers, and its implementation can be found in apps/ensindexer/src/plugins/eth/ponder.plugin.ts
.
Each plugin is an isolated ponder indexing configuration that uses the shared handlers (plus any necessary plugin-specific logic) to index its respective subregistry. See apps/ensindexer/src/plugins/base/ponder.plugin.ts
and apps/ensindexer/src/plugins/linea/ponder.plugin.ts
for examples.
Plugin-Scoping
Because plugins indexing subregistries use the shared handlers and may clobber entities created by the eth
plugin, id-generating code was abstracted to be plugin-specific. See the helpers in apps/ensindexer/src/lib/ids.ts
. In these cases, for the eth
plugin, the original behavior is left un-modified to facilitate 1:1 responses from the subgraph-compatible api.
This scoping also applies to the concept of an OwnedName
(see apps/ensindexer/src/lib/types.ts
and makeRegistrarHandlers
in apps/ensindexer/src/handlers/Registrar.ts
) — each plugin exists in the context of a name whos subnames they manage, and the shared handlers reference that name instead of a hardcoded constant ("eth"
, in the subgraph implementation).
Plugin Definition
An ENSIndexer plugin is defined by its Ponder config (network & contracts & abis & such) and the ponder handler registration code that it executes at runtime (via the activate
function).
Each plugin handler file is executed in the context of the plugin’s namespace
and ownedName
which helps the plugin customize its logic to avoid clobbering other plugins’ entities.
Ponder Plugin Integration
Contract Namespace
Ponder, by default, does not have the concept of plugins — it assumes that a config is static and that all contract names are known at compile-time. In ENSIndexer, multiple plugins reference contracts of the same name, and further namespacing is required. We namespace each contract by the Registry’s Subname
to avoid collisions (see apps/ensindexer/src/lib/plugin-helpers.ts
for reference).
Contract & Event Typing
Ponder uses the type information of contracts and their abis in the provided config to power the ponder.on('MyContract:MyEvent', ...)
api, including inferred types for contract names, event names, and event arguments.
In order to replicate this experience with plugins selected at runtime, we use some creative typing in apps/ensindexer/ponder.config.ts
to merge the possible plugin types for Ponder. With this approach we have full type inference for contract and event names/args across the app regardless of which plugins are activated at runtime.
Plugin Execution
When ENSIndexer is run, the configs for all of the active plugins (those selected by the user) are merged and ponder runs in omnichain
(soon: multichain
) mode to produce the resulting index.
@namehash/ens-deployments
This package provides configurations for each known “ENS deployment”. An ENS deployment represents a single, unified namespace of ENS names with a distinct onchain root Registry and the capability to span across multiple chains, subregistries, and offchain resources.
Each deployment is independent - for instance, the Sepolia and Holesky testnet deployments are separate from the canonical mainnet deployment. This package centralizes the contract addresses, start blocks, and other configuration needed to interact with each deployment.
ENSIndexer uses @namehash/ens-deployments
to configure its plugins and determine which are available for a given target deployment.
See the @namehash/ens-deployments
README for more context on this package & its responsibilities.
General Execution Flow
The subgraph’s codebase is not exhaustively documented or trivially readable. In some cases we’ve decided to simplify the implementation (ensuring accuracy via ens-subgraph-transition-tools) and in others we’ve elected to match the subgraph’s logic closer to 1:1.
In general, however, each handler is written in a more ponder-native way, using ponder’s drizzle-inspired entity CRUD apis, rather than the subgraph’s active-record-inspired api. It uses minimial branched or nested logic, resulting in code that is much more readable. Along the way we’ve also documented the purpose of these handlers more exhaustively, which should promote understanding and readability.
API Layer
ENSIndexer exposes three distinct API endpoints:
-
Subgraph-Compatible GraphQL (
/subgraph
)- Implements the ENS Subgraph schema and query patterns
- Enables gradual migration from existing Subgraph implementations
- Maintains compatibility with
ensjs
client library — just replace
-
Native Ponder GraphQL (
/ponder
)- Auto-generated GraphQL API from Ponder schema
- More efficient query patterns than Subgraph API, particularly for pagination
-
Ponder Client API (
/sql/*
)- SQL-based alternative to GraphQL
- End-to-end type inference
- Support for live updates
- Optimized for client application integration
Label Healing with ENSRainbow
ENSIndexer depends on ENSRainbow at runtime to handle the healing of unknown labels. This parallels the ENS Subgraph’s reliance on the graph-node’s ens.nameByHash
function.
Additional Notes in Comments
Additional implementation & background context for certain decisions are included throughout the codebase where relevant, and we encourage curious readers to browse the comments and general structure of the shared handlers & helper libs for further background.