-
Notifications
You must be signed in to change notification settings - Fork 16
refactor(ensindexer): improve ENSIndexerConfig #765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
54537f5
3f31811
3491c3c
e7aa6df
b4c181c
b9e95d0
7c29440
36e1d7a
f341c50
eebd448
375b3bc
6df54e6
7762a32
5c3b336
41ee3f5
282ff20
b25c7a4
4a9df53
d13baf8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "ensindexer": minor | ||
| --- | ||
|
|
||
| Update ENSIndexerConfig to include `ensDeployment` object. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "ensindexer": minor | ||
| --- | ||
|
|
||
| Set concrete types for each of the plugins. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"; | ||
| import { ALL_PLUGINS, type AllPluginsConfig } from "@/plugins"; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| //////// | ||
| // 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. | ||
|
|
@@ -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; | ||
|
|
||
|
|
||
| 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, | ||
|
|
@@ -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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved into a separate |
||
| const ENSIndexerConfigSchema = z | ||
| .object({ | ||
| ensDeploymentChain: EnsDeploymentChainSchema, | ||
|
|
@@ -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 | ||
|
|
||
| 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, | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm surprised this works ok? As I understand
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This works OK as invariant functions, such as Please note how I've build a small demo to illustrate that here: |
||
| 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), | ||
| ); | ||
|
|
@@ -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]; | ||
|
|
@@ -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( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you refer to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I've just discovered it might be tricky to replace reading from
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logged an issue: |
||
| config.plugins | ||
| .flatMap((pluginName) => PLUGIN_REQUIRED_DATASOURCES[pluginName]) | ||
| .flatMap((pluginName) => getPlugin(pluginName).requiredDatasources) | ||
| .map((datasourceName) => deployment[datasourceName]) | ||
| .map((datasource) => datasource.chain.id), | ||
| ); | ||
|
|
@@ -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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,7 +59,7 @@ export function prettyPrintConfig(config: ENSIndexerConfig) { | |
| ]), | ||
| ), | ||
| } as ENSIndexerConfig, | ||
| null, | ||
| (key: string, value: unknown) => (key === "abi" ? `(truncated ABI output)` : value), | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| ); | ||
| } | ||

There was a problem hiding this comment.
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.