Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
54537f5
refactor(ensindexer): improve input type on validation functions
tk-o May 30, 2025
3f31811
refactor(ensindexer): derive config params _after_ performing validat…
tk-o May 30, 2025
3491c3c
fix(ensindexer): circular types resolution
tk-o May 30, 2025
e7aa6df
refactor(ensindexer): derive `ensDeployment` config param
tk-o May 30, 2025
b4c181c
fix(ensindexer): `prettyPrintConfig`, replace abi output
tk-o May 30, 2025
b9e95d0
docs(changeset): Update ENSIndexerConfig to include `ensDeployment` o…
tk-o May 30, 2025
7c29440
fix: typo
tk-o May 30, 2025
36e1d7a
refactor(ensindexer): move requiredDatasources definition to plugins
tk-o May 30, 2025
f341c50
fix(ensindexer): load plugin handlers lazily
tk-o May 30, 2025
eebd448
refactor(ensindexer): consolidate plugin helpers
tk-o May 30, 2025
375b3bc
Merge remote-tracking branch 'origin/feat/757-761-improve-ensindexerc…
tk-o May 30, 2025
6df54e6
test(ensindexer): apply original assertions
tk-o May 30, 2025
7762a32
fix(ensindexer): align plugin type constraints
tk-o May 30, 2025
5c3b336
docs(changeset): Set concrete types for each of the plugins.
tk-o May 30, 2025
41ee3f5
fix(ensindexer): align plugin type constraints
tk-o May 31, 2025
282ff20
Merge remote-tracking branch 'origin/main' into feat/757-761-improve-…
tk-o Jun 2, 2025
b25c7a4
Apply suggestions from code review
tk-o Jun 3, 2025
4a9df53
refactor(ensindexer): use "ponder config" related terms for plugins c…
tk-o Jun 3, 2025
d13baf8
apply pr feedback
tk-o Jun 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ninety-actors-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensindexer": minor
---

Update ENSIndexerConfig to include `ensDeployment` object.
5 changes: 5 additions & 0 deletions .changeset/plain-steaks-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensindexer": minor
---

Set concrete types for each of the plugins.
18 changes: 3 additions & 15 deletions apps/ensindexer/ponder.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,15 @@ import config from "@/config";
import type { ENSIndexerConfig } from "@/config/types";
import { prettyPrintConfig } from "@/lib/lib-config";
import { mergePonderConfigs } from "@/lib/merge-ponder-configs";
import type { MergedTypes } from "@/lib/plugin-helpers";

import basenamesPlugin from "@/plugins/basenames/basenames.plugin";
import lineaNamesPlugin from "@/plugins/lineanames/lineanames.plugin";
import subgraphPlugin from "@/plugins/subgraph/subgraph.plugin";
import threednsPlugin from "@/plugins/threedns/threedns.plugin";
Comment on lines -5 to -10
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This refactor enabled creation of getPlugin(pluginName: PluginName) helper, which will be used in validation logic.

import { ALL_PLUGINS, type AllPluginsConfig } from "@/plugins";
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Decoupling plugins list from ponder.config.ts file is a step towards having plugin configuration managed through ENSIndexerConfig builder.


////////
// First, generate `MergedPonderConfig` type representing the merged types of each plugin's `config`,
// so ponder's typechecking of the indexing handlers and their event arguments is correct, regardless
// of which plugins are actually active at runtime.
////////

export const ALL_PLUGINS = [
subgraphPlugin,
basenamesPlugin,
lineaNamesPlugin,
threednsPlugin,
] as const;

export type MergedPonderConfig = MergedTypes<(typeof ALL_PLUGINS)[number]["config"]> & {
export type MergedPonderConfig = AllPluginsConfig & {
/**
* NOTE: we inject additional values (ones that change the behavior of the indexing logic) into the
* Ponder config in order to alter the ponder-generated build id when these additional options change.
Expand All @@ -47,7 +35,7 @@ const activePlugins = ALL_PLUGINS.filter((plugin) => config.plugins.includes(plu

// combine each plugins' config into a MergedPonderConfig
const ponderConfig = activePlugins.reduce(
(memo, plugin) => mergePonderConfigs(memo, plugin.config),
(memo, plugin) => mergePonderConfigs(memo, plugin.createPonderConfig(config)),
{},
) as MergedPonderConfig;

Expand Down
73 changes: 45 additions & 28 deletions apps/ensindexer/src/config/config.schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { parse as parseConnectionString } from "pg-connection-string";
import { prettifyError, z } from "zod/v4";

import { ENSIndexerConfig, ENSIndexerEnvironment } from "@/config/types";
import { derive_ensDeployment, derive_isSubgraphCompatible } from "@/config/derived-params";
import type { ENSIndexerConfig, ENSIndexerEnvironment } from "@/config/types";
import {
invariant_globalBlockrange,
invariant_requiredDatasources,
Expand Down Expand Up @@ -154,29 +155,6 @@ const DatabaseUrlSchema = z.union(
},
);

const derive_isSubgraphCompatible = <
CONFIG extends Pick<
ENSIndexerConfig,
"plugins" | "healReverseAddresses" | "indexAdditionalResolverRecords"
>,
>(
config: CONFIG,
): CONFIG & { isSubgraphCompatible: boolean } => {
// 1. only the subgraph plugin is active
const onlySubgraphPluginActivated =
config.plugins.length === 1 && config.plugins[0] === PluginName.Subgraph;

// 2. healReverseAddresses = false
// 3. indexAdditionalResolverRecords = false
const indexingBehaviorIsSubgraphCompatible =
!config.healReverseAddresses && !config.indexAdditionalResolverRecords;

return {
...config,
isSubgraphCompatible: onlySubgraphPluginActivated && indexingBehaviorIsSubgraphCompatible,
};
};

Comment on lines -157 to -179
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Moved into a separate derived-params.ts file.

const ENSIndexerConfigSchema = z
.object({
ensDeploymentChain: EnsDeploymentChainSchema,
Expand All @@ -192,13 +170,52 @@ const ENSIndexerConfigSchema = z
rpcConfigs: RpcConfigsSchema,
databaseUrl: DatabaseUrlSchema,
})
// inject ENSIndexerConfig.isSubgraphCompatible
.transform(derive_isSubgraphCompatible)
// perform invariant checks
/**
* Invariant enforcement
*
* We enforce invariants across multiple values parsed with `ENSIndexerConfigSchema`
* by calling `.check()` function with relevant invariant-enforcing logic.
* Each such function has access to config values that were already parsed.
* If you need to ensure certain config value permutation, say across `ensDeploymentChain`
* and `plugins` values, you can define the `.check()` function callback with the following
* input param:
*
* ```ts
* ctx: ZodCheckFnInput<Pick<ENSIndexerConfig, "ensDeploymentChain" | "plugins">>
* ```
*
* This way, the invariant logic can access all information it needs, while keeping room
* for the derived values of ENSIndexerConfig to be computed after all `.check()`s.
*/
.check(invariant_requiredDatasources)
.check(invariant_rpcConfigsSpecifiedForIndexedChains)
.check(invariant_globalBlockrange)
.check(invariant_validContractConfigs);
.check(invariant_validContractConfigs)
/**
* Derived configuration
*
* We create new configuration parameters from the values parsed with `ENSIndexerConfigSchema`.
* This way, we can include complex configuration objects, for example, `ensDeployment` that was
* derived from `ensDeploymentChain` and relevant SDK helper method, and attach result value to
* ENSIndexerConfig object. For example, we can get a slice of already parsed and validated
* ENSIndexerConfig values, and return this slice PLUS the derived configuration properties.
*
* ```ts
* function derive_isSubgraphCompatible<
* CONFIG extends Pick<
* ENSIndexerConfig,
* "plugins" | "healReverseAddresses" | "indexAdditionalResolverRecords"
* >,
* >(config: CONFIG): CONFIG & { isSubgraphCompatible: boolean } {
* return {
* ...config,
* isSubgraphCompatible: true // can use some complex logic to calculate the final outcome
* }
* }
* ```
*/
.transform(derive_ensDeployment)
.transform(derive_isSubgraphCompatible);

/**
* Builds the ENSIndexer configuration object from an ENSIndexerEnvironment object
Expand Down
43 changes: 43 additions & 0 deletions apps/ensindexer/src/config/derived-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { ENSIndexerConfig } from "@/config/types";
import { type ENSDeploymentCommonType, getENSDeployment } from "@ensnode/ens-deployments";
import { PluginName } from "@ensnode/ensnode-sdk";

/**
* Derived `isSubgraphCompatible` config param based on validated ENSIndexerConfig object.
*/
export const derive_isSubgraphCompatible = <
CONFIG extends Pick<
ENSIndexerConfig,
"plugins" | "healReverseAddresses" | "indexAdditionalResolverRecords"
>,
>(
config: CONFIG,
): CONFIG & { isSubgraphCompatible: boolean } => {
// 1. only the subgraph plugin is active
const onlySubgraphPluginActivated =
config.plugins.length === 1 && config.plugins[0] === PluginName.Subgraph;

// 2. healReverseAddresses = false
// 3. indexAdditionalResolverRecords = false
const indexingBehaviorIsSubgraphCompatible =
!config.healReverseAddresses && !config.indexAdditionalResolverRecords;

return {
...config,
isSubgraphCompatible: onlySubgraphPluginActivated && indexingBehaviorIsSubgraphCompatible,
};
};

/**
* Derived `ensDeployment` config param based on validated ENSIndexerConfig object.
*/
export const derive_ensDeployment = <CONFIG extends Pick<ENSIndexerConfig, "ensDeploymentChain">>(
config: CONFIG,
): CONFIG & { ensDeployment: ENSDeploymentCommonType } => {
const ensDeployment = getENSDeployment(config.ensDeploymentChain);

return {
...config,
ensDeployment,
};
};
19 changes: 15 additions & 4 deletions apps/ensindexer/src/config/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Blockrange } from "@/lib/types";
import type { ENSDeployments } from "@ensnode/ens-deployments";
import type {
ENSDeployment,
ENSDeploymentChain,
ENSDeploymentCommonType,
} from "@ensnode/ens-deployments";
import type { PluginName } from "@ensnode/ensnode-sdk";

/**
Expand Down Expand Up @@ -30,11 +34,18 @@ export interface RpcConfig {
*/
export interface ENSIndexerConfig {
/**
* The ENS Deployment that ENSIndexer is targeting, defaulting to 'mainnet' (DEFAULT_ENS_DEPLOYMENT_CHAIN).
* The ENS Deployment that ENSIndexer is indexing, defaulting to 'mainnet' (DEFAULT_ENS_DEPLOYMENT_CHAIN).
*
* See {@link ENSDeployments} for available deployments.
* See {@link ENSDeploymentChain} for available deployment chains.
*/
ensDeploymentChain: keyof typeof ENSDeployments;
ensDeploymentChain: ENSDeploymentChain;

/**
* Details of the ENS Deployment on `ensDeploymentChain`.
*
* See {@link ENSDeployment} for the deployment type.
*/
ensDeployment: ENSDeploymentCommonType;

/**
* An ENSAdmin url, defaulting to the public instance https://admin.ensnode.io (DEFAULT_ENSADMIN_URL).
Expand Down
37 changes: 22 additions & 15 deletions apps/ensindexer/src/config/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,26 @@ import { z } from "zod/v4";

import type { ENSIndexerConfig } from "@/config/types";
import { uniq } from "@/lib/lib-helpers";
import { PLUGIN_REQUIRED_DATASOURCES } from "@/plugins";
import { getPlugin } from "@/plugins";
import { DatasourceName, getENSDeployment } from "@ensnode/ens-deployments";
import { PluginName } from "@ensnode/ensnode-sdk";
import { Address, isAddress } from "viem";

// type alias to highlight the input param of Zod's check() method
type ZodCheckFnInput<T> = z.core.ParsePayload<T>;

// Invariant: specified plugins' datasources are available in the specified ensDeploymentChain's ENSDeployment
export function invariant_requiredDatasources(ctx: z.core.ParsePayload<ENSIndexerConfig>) {
export function invariant_requiredDatasources(
ctx: ZodCheckFnInput<Pick<ENSIndexerConfig, "ensDeploymentChain" | "plugins">>,
) {
const { value: config } = ctx;

const deployment = getENSDeployment(config.ensDeploymentChain);
const allPluginNames = Object.keys(PLUGIN_REQUIRED_DATASOURCES) as PluginName[];
const availableDatasourceNames = Object.keys(deployment) as DatasourceName[];
const activePluginNames = allPluginNames.filter((pluginName) =>
config.plugins.includes(pluginName),
);
const ensDeployment = getENSDeployment(config.ensDeploymentChain);
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'm surprised this works ok? As I understand invariant_requiredDatasources is called before config is finished being built?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This works OK as invariant functions, such as invariant_requiredDatasources, are applied after the input object was already parsed into correct individual types.

Please note how Pick type is applied onto the ENSIndexerConfig type. This way, we can ensure the invariant function gets all data it needs from a slice of the whole config. The parsing process ended, but we still apply invariants, and add derived config values to the final config object.

I've build a small demo to illustrate that here:
image

const availableDatasourceNames = Object.keys(ensDeployment) as DatasourceName[];
const activePluginNames = config.plugins;

// validate that each active plugin's requiredDatasources are available in availableDatasourceNames
for (const pluginName of activePluginNames) {
const requiredDatasources = PLUGIN_REQUIRED_DATASOURCES[pluginName];
const { requiredDatasources } = getPlugin(pluginName);
const hasRequiredDatasources = requiredDatasources.every((datasourceName) =>
availableDatasourceNames.includes(datasourceName),
);
Expand All @@ -43,14 +44,14 @@ export function invariant_requiredDatasources(ctx: z.core.ParsePayload<ENSIndexe

// Invariant: rpcConfig is specified for each indexed chain
export function invariant_rpcConfigsSpecifiedForIndexedChains(
ctx: z.core.ParsePayload<ENSIndexerConfig>,
ctx: ZodCheckFnInput<Pick<ENSIndexerConfig, "ensDeploymentChain" | "plugins" | "rpcConfigs">>,
) {
const { value: config } = ctx;

const deployment = getENSDeployment(config.ensDeploymentChain);

for (const pluginName of config.plugins) {
const datasourceNames = PLUGIN_REQUIRED_DATASOURCES[pluginName];
const datasourceNames = getPlugin(pluginName).requiredDatasources;

for (const datasourceName of datasourceNames) {
const { chain } = deployment[datasourceName];
Expand All @@ -67,15 +68,19 @@ export function invariant_rpcConfigsSpecifiedForIndexedChains(
}

// Invariant: if a global blockrange is defined, only one network is indexed
export function invariant_globalBlockrange(ctx: z.core.ParsePayload<ENSIndexerConfig>) {
export function invariant_globalBlockrange(
ctx: ZodCheckFnInput<
Pick<ENSIndexerConfig, "globalBlockrange" | "ensDeploymentChain" | "plugins">
>,
) {
const { value: config } = ctx;
const { globalBlockrange } = config;

if (globalBlockrange.startBlock !== undefined || globalBlockrange.endBlock !== undefined) {
const deployment = getENSDeployment(config.ensDeploymentChain);
const indexedChainIds = uniq(
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 think I saw another helper function we've built now for getting the set of unique chainIds being indexed? Seems nice to reuse it here if possible?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think you refer to getIndexedChainIds function was added in this very PR inside plugin-helpers.ts. Will apply it here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

getIndexedChainIds has implicit dependency on config value from import config from "@/config".

I've just discovered it might be tricky to replace reading from import config from "@/config"; and keep typescript inferred types working. It all comes back to ponder config expecting literal values.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

config.plugins
.flatMap((pluginName) => PLUGIN_REQUIRED_DATASOURCES[pluginName])
.flatMap((pluginName) => getPlugin(pluginName).requiredDatasources)
.map((datasourceName) => deployment[datasourceName])
.map((datasource) => datasource.chain.id),
);
Expand All @@ -102,7 +107,9 @@ export function invariant_globalBlockrange(ctx: z.core.ParsePayload<ENSIndexerCo
}

// Invariant: all contracts have a valid ContractConfig defined
export function invariant_validContractConfigs(ctx: z.core.ParsePayload<ENSIndexerConfig>) {
export function invariant_validContractConfigs(
ctx: ZodCheckFnInput<Pick<ENSIndexerConfig, "ensDeploymentChain">>,
) {
const { value: config } = ctx;

const deployment = getENSDeployment(config.ensDeploymentChain);
Expand Down
2 changes: 1 addition & 1 deletion apps/ensindexer/src/lib/lib-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function prettyPrintConfig(config: ENSIndexerConfig) {
]),
),
} as ENSIndexerConfig,
null,
(key: string, value: unknown) => (key === "abi" ? `(truncated ABI output)` : value),
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Quite handy to avoid printing very long ABIs into stdout for ENSIndexer container.

2,
);
}
Loading