Skip to content

Conversation

@tk-o
Copy link
Contributor

@tk-o tk-o commented Jun 8, 2025

This PR addresses a challenge of defining ENSIndexer plugins with proper type inference.

Related issue:

Review suggestions:

  • turn Diff view: Hide whitespace option on

Review order:

  1. apps/ensindexer/src/lib/plugin-helpers.ts
    • contains the most important set of changes
  2. apps/ensindexer/src/lib/ponder-helpers.ts
    • hosts two functions moved from the plugin-helpers.ts file
  3. apps/ensindexer/src/plugins/index.ts
    • new datasource-related helpers
  4. apps/ensindexer/src/plugins/event-handlers.ts
    • new way of declaring ENSIndexer event handlers per plugin
  5. apps/ensindexer/src/plugins/basenames/plugin.ts & apps/ensindexer/src/plugins/basenames/event-handlers.ts
  6. apps/ensindexer/src/plugins/lineanames/plugin.ts & apps/ensindexer/src/plugins/lineanames/event-handlers.ts
  7. apps/ensindexer/src/plugins/subgraph/plugin.ts & apps/ensindexer/src/plugins/subgraph/event-handlers.ts
  8. apps/ensindexer/src/plugins/threedns/plugin.ts & apps/ensindexer/src/plugins/threedns/event-handlers.ts
  9. apps/ensindexer/ponder.config.ts
    • uses new API for attaching event handlers for active plugins
  10. apps/ensindexer/test/*
  11. docs/*

Key changes:

  • decouple plugin handlers from plugin config file
    • now, all indexing handlers for a plugin must be exported from its own file
      • let's take the basenames plugin as an example: its handlers file must be present at apps/ensindexer/src/plugins/basenames/event-handlers.ts
      • the handlers file must export a function that will later be called from ponder.config.ts for each active plugin
    • activating indexing handlers for a plugin can be done with a new helper function called attachPluginEventHandlers
  • introduce buildPlugin function with automated type inference
    • now, every plugin file returns a results of buildPlugin function
    • ponder.config.ts file can access Ponder config object for each active plugin by calling its createPonderConfig method

tk-o added 4 commits June 7, 2025 15:42
Decouple handlers from plugins. At least for the eager type inference part.
This way, we decouple the plugin handlers definition from the plugin config definition. It allows breaking circular type inference.
@changeset-bot
Copy link

changeset-bot bot commented Jun 8, 2025

🦋 Changeset detected

Latest commit: a1ce74d

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

This PR includes changesets to release 10 packages
Name Type
ensindexer Minor
ensadmin Minor
ensrainbow Minor
@ensnode/datasources Minor
@ensnode/ensrainbow-sdk Minor
@ensnode/ponder-metadata Minor
@ensnode/ensnode-schema Minor
@ensnode/ponder-subgraph Minor
@ensnode/ensnode-sdk Minor
@ensnode/shared-configs 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

vercel bot commented Jun 8, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
admin.ensnode.io ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 30, 2025 0:11am
ensnode.io ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 30, 2025 0:11am
ensrainbow.io ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 30, 2025 0:11am

Copy link
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

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

@tk-o Thanks for the updates. I've shared many suggestions for improvement. I'm open to following up on these suggestions in other future PRs. Please make a special effort to ensure none of the suggestions shared are forgotten and all ultimately receive a response.

You're welcome to merge this PR when ready. Please decide for yourself if Matt or I should review this PR once more. Either option is ok from my perspective.

/**
* Attach plugin's event handlers for indexing.
*
* Note: this function is called when the plugin is active.
Copy link
Member

Choose a reason for hiding this comment

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

Please clarify the language here. It's not sufficiently clear or precise. There's too many ways to interpret this.

As I understand, this is what you're trying to say:

Suggested change
* Note: this function is called when the plugin is active.
* Note: this function is called if and only if the plugin is being activated.

Is that right? Or are you trying to say something else?

...((chainId === 31337 || chainId === 1337) && { disableCache: true }),
} satisfies NetworkConfig,
};
createPonderConfig(
Copy link
Member

Choose a reason for hiding this comment

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

Currently we're mixing together our plugin definitions with a lot of Ponder-specific details. It seems there should be a more simple design?

Why couldn't we completely remove explicit reference to Ponder things when we define a plugin, and instead define our own types for our plugins?

For example, imagine we completely separated the following into distinct layers:

  1. Defining an ENSIndexer plugin (using our own types).
  2. Taking an ENSIndexer plugin definition and converting it into a PonderConfig.

In layer (1) listed above, we might:
A. Completely remove the createPonderConfig function.
B. Add a function for getIndexedContracts that returns only the contracts value that is currently returned by createPonderConfig.
C. Add a function for getEventHandlers that returns the record type for mapping between (namespaced) contract names and event handler callback functions. Importantly: this wouldn't ever include an import or call to ponder. Please see my other more detailed feedback about this idea in another comment.

In layer (2) listed above, we might:
A. Take all the values returned by getIndexedContracts for all active plugins and..:
i. Identify the chains being requested for indexing, and then getting the rpcConfigs from the ENSIndexerConfig as needed to build the networks object needed by Ponder.
ii. Build the contracts object needed by Ponder.

Only layer (2) would then need all these complex looking type definitions that Ponder needs, right?

We could refer to Layer (1) as our "Plugin" layer and Layer (2) as our "PonderConfig" layer.

In other words, Layer (1) doesn't need to know about PonderConfigs, because Layer (2) takes the full responsibility for translating from our "Plugins" into the "PonderConfig" that we ultimately pass to Ponder.

*
* @returns network configuration based on the contract
*/
export function networkConfigForContract<CONTRACT_CONFIG extends ContractConfig>(
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to use a type template here for CONTRACT_CONFIG? Seems we should be fine to remove the use of type templates here?

* Builds a ponder#NetworksConfig for a single, specific chain.
*
* @param {number} chainId
* @param rpcConfigs a dictionary of RPC configurations, grouped by chainId
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* @param rpcConfigs a dictionary of RPC configurations, grouped by chainId
* @param rpcConfigs a dictionary of RPC configurations, keyed by chainId

@tk-o tk-o merged commit f4d6a6e into main Jun 30, 2025
7 checks passed
@tk-o tk-o deleted the feat/ensindexer-define-plugin branch June 30, 2025 12:28
@github-actions github-actions bot mentioned this pull request Jun 25, 2025
Copy link
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

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

Sharing a few additional ideas for future follow up on enhancements to our plugin system.

import { attachLineanamesPluginEventHandlers } from "@/plugins/lineanames/event-handlers";
import { attachSubgraphPluginEventHandlers } from "@/plugins/subgraph/event-handlers";
import { attachThreeDNSPluginEventHandlers } from "@/plugins/threedns/event-handlers";
// Shared Resolver Handlers
Copy link
Member

Choose a reason for hiding this comment

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

There's a future opportunity for us to strengthen the documentation for specifically what it means for handlers to be "shared". Ideally we could also connect this idea to why some handlers are are associated with namespaced contracts and others aren't.

import { attachSharedResolverHandlers } from "@/plugins/shared/Resolver";

// Subgraph Handlers
import { attachSubgraphNameWrapperEventHandlers } from "./subgraph/handlers/NameWrapper";
Copy link
Member

Choose a reason for hiding this comment

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

It seems attractive to separate the following ideas:

  1. The definition of a mapping from an event name with a (namespaced) contract to an event handler function (just a named import for each event handler, not the implementation of those event handlers). This should never make any reference to ponder.on or import ponder.
  2. The registration of the relationship defined in point (1) above with ponder using ponder.on. This imports ponder and calls ponder.on.
  3. The implementation of the event handlers defined in point (1) above. These don't import ponder.

Copy link
Collaborator

Choose a reason for hiding this comment

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

i'm not sure an additional level of abstraction between plugins and ponder would help but we can explore what that would look like. also it's the usage of ponder.on that gives us the inferred types, so another level of abstraction will give us additional type wrangling to do that seems less productive

* handlers. ponder.config.ts will call {@link attachPluginEventHandlers} to conditionally
* register a specific plugin's handlers with Ponder.
*
* NOTE: defined separate from plugin.ts to avoid possible circular dependencies
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice if we could be more specific about exactly what is creating the circular dependencies. As I understand these are caused by our calls to ponder.on. Please see my other comment on this file suggesting to split related logic into 3 distinct layers.

attachSubgraphRegistrarEventHandlers,
attachSubgraphRegistryEventHandlers,

// NOTE: shared Resolver handlers
Copy link
Member

Choose a reason for hiding this comment

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

This is an additional comment on the desire to strengthen our docs and language related to the use of the word "shared" here.

Is it correct to assume that ideas here include how the "shared Resolver handlers" are fundamentally a per-chain concept rather than a per-registry / subregistry concept?

It would be great to better document these ideas.

Copy link
Collaborator

Choose a reason for hiding this comment

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

let's avoid documenting it in every possible location, perhaps we can write some page in the docs and just link to it at these callsites?

);

ponder.on(ns("BaseRegistrar:Transfer"), async ({ context, event }) => {
ponder.on(namespaceContract(pluginName, "BaseRegistrar:Transfer"), async ({ context, event }) => {
Copy link
Member

Choose a reason for hiding this comment

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

Hmm. it seems we're not actually namespacing contracts here. As I understand, we should rename namespaceContract to namespaceEvent. Is that fair?

One idea is to refine our strategy here so that we don't pass in event names such as "BaseRegistrar:Transfer" but instead separate params for "BaseRegistrar" (the contract name), and "Transfer" as the event name, and then pluginName as a 3rd param for the contract namespace.

Then this could be called namespaceEvent. Or perhaps better: make the contract namespace param optional and rename namespaceContract to something like buildEventName or something.

Advice appreciated.

Copy link
Collaborator

Choose a reason for hiding this comment

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

let's keep namespace to imply namespacing behavior, very important here

namespaceEvent or namespaceEventName seems fine

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.

4 participants