From 5a5f3c0caf75a599a861703d45d9e1a4c096ff30 Mon Sep 17 00:00:00 2001 From: shrugs Date: Sun, 3 May 2026 15:50:54 -0500 Subject: [PATCH 1/4] refactor: eliminate zero-address placeholder contracts; merge .eth RegistrarControllers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes `LegacyEthRegistrarController`, `WrappedEthRegistrarController`, and `UniversalRegistrarRenewalWithReferrer` placeholder entries (`address: zeroAddress, startBlock: 0`) from sepolia-v2 and `UniversalRegistrarRenewalWithReferrer` from ens-test-env. These were registered with Ponder, which dragged each chain's effective startBlock to 0 and caused `historicalTotalBlocks` to overshoot by ~3.7M, producing `Block 14473749 not found` on sepolia-v2. In `registrars`, `subgraph`, and `ensv2` plugins, replaces per-controller Ponder contract entries with a single `RegistrarController` entry per chain that uses `AnyRegistrarControllerABI` (now also includes `UniversalRegistrarRenewalWithReferrer`). Controller addresses are combined into one chain entry via the new `mergedChainConfigForContracts` helper, with `pickContracts` for namespace-conditional lookup. Handlers dispatch by long-form event signature, mirroring `ensv2/handlers/ensv1/RegistrarController.ts`. Reverts the zero-address skip in `buildIndexedBlockranges` from #2045 — the underlying placeholders no longer exist. `getContractsByManagedName` now treats the optional ENSRoot controllers as `maybeGetDatasourceContract` lookups and filters absent ones. Closes #2048: the `ensv2` plugin's previous `chain: { …spread, …spread, …spread }` pattern silently overwrote each controller's chain-id-keyed config (only the last spread per chain id survived; on mainnet only `UnwrappedEthRegistrarController` was indexed). The new `mergedChainConfigForContracts` helper combines addresses correctly via `address: Address[]`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../eliminate-zero-address-placeholders.md | 13 ++ ...ip-zero-address-placeholders-blockrange.md | 5 - apps/ensindexer/src/lib/ponder-helpers.ts | 62 ++++++++ apps/ensindexer/src/plugins/ensv2/plugin.ts | 47 +++--- .../handlers/Ethnames_RegistrarController.ts | 147 +++++++----------- ...s_UniversalRegistrarRenewalWithReferrer.ts | 54 ------- .../src/plugins/registrars/event-handlers.ts | 6 +- .../src/plugins/registrars/plugin.ts | 41 ++--- .../plugins/subgraph/handlers/Registrar.ts | 101 ++++++------ .../subgraph/plugins/subgraph/plugin.ts | 32 ++-- packages/datasources/src/ens-test-env.ts | 9 -- .../src/lib/AnyRegistrarControllerABI.ts | 2 + packages/datasources/src/sepolia-v2.ts | 28 +--- .../shared/config/indexed-blockranges.test.ts | 36 ----- .../src/shared/config/indexed-blockranges.ts | 7 - .../ensnode-sdk/src/shared/managed-names.ts | 16 +- 16 files changed, 245 insertions(+), 361 deletions(-) create mode 100644 .changeset/eliminate-zero-address-placeholders.md delete mode 100644 .changeset/skip-zero-address-placeholders-blockrange.md delete mode 100644 apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts diff --git a/.changeset/eliminate-zero-address-placeholders.md b/.changeset/eliminate-zero-address-placeholders.md new file mode 100644 index 0000000000..42bcf8cf24 --- /dev/null +++ b/.changeset/eliminate-zero-address-placeholders.md @@ -0,0 +1,13 @@ +--- +"@ensnode/datasources": patch +"ensindexer": patch +"@ensnode/ensnode-sdk": patch +--- + +Eliminate zero-address placeholder contracts; merge ENSRoot registrar controllers into a single Ponder contract entry per chain. + +- `@ensnode/datasources`: removed `LegacyEthRegistrarController`, `WrappedEthRegistrarController`, and `UniversalRegistrarRenewalWithReferrer` placeholder entries from the `sepolia-v2` namespace, and `UniversalRegistrarRenewalWithReferrer` from `ens-test-env`. These were typed-but-unindexable entries pinned at `address: zeroAddress, startBlock: 0`. `AnyRegistrarControllerABI` now also includes the `UniversalRegistrarRenewalWithReferrer` ABI so its `RenewalReferred` event participates in the merged controller dispatch. +- `ensindexer`: + - `registrars` and `subgraph` plugins: the four per-controller Ponder contract entries (`Legacy`, `Wrapped`, `Unwrapped`, `URRWR`) are replaced by a single `RegistrarController` entry per chain that uses the merged `AnyRegistrarControllerABI`. Controller addresses are combined into one chain entry via the new `mergedChainConfigForContracts` helper; controllers absent from the active namespace contribute no address. Handlers dispatch by long-form event signature, mirroring `apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts`. This removes the namespace-conditional contract-name typesystem problem entirely — the merged entry is always present because at least one controller (`UnwrappedEthRegistrarController`) exists in every namespace. + - `ensv2` plugin: the `RegistrarController` entry's per-chain configs are now built via `mergedChainConfigForContracts` instead of spreading multiple `chainConfigForContract(...)` results into the same `chain: {}` map. The previous spread pattern silently overwrote earlier entries for the same chain id (e.g. on mainnet only `UnwrappedEthRegistrarController` survived; Legacy and Wrapped were dropped). Fixes #2048. +- `@ensnode/ensnode-sdk`: reverted the zero-address skip in `buildIndexedBlockranges` from #2045 — the underlying placeholders no longer exist, so the workaround is unnecessary. `getContractsByManagedName` now treats the optional ENSRoot controllers as `maybeGetDatasourceContract` lookups and filters absent ones, matching the existing Basenames/Lineanames pattern. Together these fix the `historicalTotalBlocks` overshoot that produced `Block 14473749 not found` on sepolia-v2. diff --git a/.changeset/skip-zero-address-placeholders-blockrange.md b/.changeset/skip-zero-address-placeholders-blockrange.md deleted file mode 100644 index c1d45c2347..0000000000 --- a/.changeset/skip-zero-address-placeholders-blockrange.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@ensnode/ensnode-sdk": patch ---- - -`buildIndexedBlockranges` now skips contracts whose `address` is the zero address. These exist in some namespaces purely as typesystem placeholders for plugin-required datasource fields and are not actually indexed by Ponder; including their `startBlock: 0` was incorrectly dragging the chain's indexed blockrange lower bound to `0`, which propagated into `ChainIndexingStatusSnapshot` and produced repeated `latestIndexedBlock must be before or same as backfillEndBlock` validation errors during backfill. diff --git a/apps/ensindexer/src/lib/ponder-helpers.ts b/apps/ensindexer/src/lib/ponder-helpers.ts index c03e259fa0..662245c199 100644 --- a/apps/ensindexer/src/lib/ponder-helpers.ts +++ b/apps/ensindexer/src/lib/ponder-helpers.ts @@ -150,6 +150,68 @@ export function chainConfigForContract( }; } +/** + * Picks contracts from a datasource's `contracts` map by name, dropping any that are absent + * at runtime — e.g. namespace-conditional contracts that don't exist in the active namespace. + * + * Useful for collecting contracts to pass to {@link mergedChainConfigForContracts}. + */ +export function pickContracts( + contracts: Record, + names: readonly string[], +): ContractConfig[] { + return names + .map((name) => contracts[name] as ContractConfig | undefined) + .filter((c): c is ContractConfig => !!c); +} + +/** + * Builds a single Ponder `chain: { [chainId]: { address, startBlock, endBlock } }` entry that + * spans multiple contracts on the same chain (e.g. all of the .eth RegistrarControllers). + * + * Use this when one Ponder contract entry should index events from multiple onchain contracts + * that share an ABI. + * + * - `address` is the union of all defined contract addresses on this chain. + * - `startBlock` is the earliest contract `startBlock`. + * - `endBlock` is the latest contract `endBlock` if every contract specifies one, otherwise undefined. + * + * The result is then constrained against `globalBlockrange` like {@link chainConfigForContract}. + * Pass `contracts` as an array; callers can use `.filter(...)` to drop namespace-conditional ones. + */ +export function mergedChainConfigForContracts( + globalBlockrange: BlockNumberRange, + chainId: number, + contracts: readonly ContractConfig[], +) { + const addresses = contracts.flatMap((c) => + Array.isArray(c.address) ? c.address : c.address ? [c.address] : [], + ); + + const minStartBlock = contracts.reduce( + (memo, c) => Math.min(memo, c.startBlock), + Number.POSITIVE_INFINITY, + ); + + const allHaveEnd = contracts.every((c) => c.endBlock !== undefined); + const maxEndBlock = allHaveEnd + ? contracts.reduce((memo, c) => Math.max(memo, c.endBlock as number), 0) + : undefined; + + const { startBlock, endBlock } = constrainBlockrange( + globalBlockrange, + buildBlockNumberRange(minStartBlock, maxEndBlock), + ); + + return { + [chainId.toString()]: { + address: addresses, + startBlock, + endBlock, + }, + }; +} + /** * TODO */ diff --git a/apps/ensindexer/src/plugins/ensv2/plugin.ts b/apps/ensindexer/src/plugins/ensv2/plugin.ts index 237bc844d8..836efe7d5d 100644 --- a/apps/ensindexer/src/plugins/ensv2/plugin.ts +++ b/apps/ensindexer/src/plugins/ensv2/plugin.ts @@ -22,6 +22,8 @@ import { constrainBlockrange, getRequiredDatasources, maybeGetDatasources, + mergedChainConfigForContracts, + pickContracts, } from "@/lib/ponder-helpers"; export const pluginName = PluginName.ENSv2; @@ -221,51 +223,38 @@ export default createPlugin({ /////////////////////////////////// // Ethnames Registrar Controllers /////////////////////////////////// - ...chainConfigForContract( - config.globalBlockrange, - ensroot.chain.id, - ensroot.contracts.LegacyEthRegistrarController, - ), - ...chainConfigForContract( - config.globalBlockrange, - ensroot.chain.id, - ensroot.contracts.WrappedEthRegistrarController, - ), - ...chainConfigForContract( + ...mergedChainConfigForContracts( config.globalBlockrange, ensroot.chain.id, - ensroot.contracts.UnwrappedEthRegistrarController, + pickContracts(ensroot.contracts, [ + "LegacyEthRegistrarController", + "WrappedEthRegistrarController", + "UnwrappedEthRegistrarController", + ]), ), /////////////////////////////////// // Basenames Registrar Controllers /////////////////////////////////// - ...(basenames && { - ...chainConfigForContract( - config.globalBlockrange, - basenames.chain.id, - basenames.contracts.EARegistrarController, - ), - ...chainConfigForContract( - config.globalBlockrange, - basenames.chain.id, - basenames.contracts.RegistrarController, - ), - ...chainConfigForContract( + ...(basenames && + mergedChainConfigForContracts( config.globalBlockrange, basenames.chain.id, - basenames.contracts.UpgradeableRegistrarController, - ), - }), + pickContracts(basenames.contracts, [ + "EARegistrarController", + "RegistrarController", + "UpgradeableRegistrarController", + ]), + )), //////////////////////////////////// // Lineanames Registrar Controllers //////////////////////////////////// ...(lineanames && - chainConfigForContract( + mergedChainConfigForContracts( config.globalBlockrange, lineanames.chain.id, - lineanames.contracts.EthRegistrarController, + pickContracts(lineanames.contracts, ["EthRegistrarController"]), )), }, }, diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts index b795617940..d099dd91bf 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts @@ -16,19 +16,23 @@ import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { handleRegistrarControllerEvent } from "../../shared/lib/registrar-controller-events"; +import { handleUniversalRegistrarRenewalEvent } from "../../shared/lib/universal-registrar-renewal-with-referrer-events"; /** - * Registers event handlers with Ponder. + * Registers event handlers for the various .eth RegistrarControllers. */ export default function () { const pluginName = PluginName.Registrars; /** - * Ethnames_LegacyEthRegistrarController Event Handlers + * NameRegistered (yes base cost, no premium, no referral) + * - LegacyEthRegistrarController */ - addOnchainEventListener( - namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController:NameRegistered"), + namespaceContract( + pluginName, + "Ethnames_RegistrarController:NameRegistered(string name, bytes32 indexed label, address indexed owner, uint256 cost, uint256 expires)", + ), async ({ context, event }) => { const { id, @@ -43,10 +47,6 @@ export default function () { const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; - /** - * Ethnames_LegacyEthRegistrarController does not implement premiums, - * however, it implements base cost. - */ const baseCost = priceEth(event.args.cost); const premium = priceEth(0n); const total = baseCost; @@ -56,10 +56,6 @@ export default function () { total, } satisfies RegistrarActionPricingAvailable; - /** - * Ethnames_LegacyEthRegistrarController does not implement referrals or - * emits a referrer in events. - */ const referral = { encodedReferrer: null, decodedReferrer: null, @@ -75,8 +71,14 @@ export default function () { }, ); + /** + * WrappedEthRegistrarController: NameRegistered (premium, no referral) + */ addOnchainEventListener( - namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController:NameRenewed"), + namespaceContract( + pluginName, + "Ethnames_RegistrarController:NameRegistered(string name, bytes32 indexed label, address indexed owner, uint256 baseCost, uint256 premium, uint256 expires)", + ), async ({ context, event }) => { const { id, @@ -91,25 +93,15 @@ export default function () { const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; - /** - * Ethnames_LegacyEthRegistrarController does not implement premiums, - * however, it implements base cost. - * - * Premium for renewals is always 0 anyway. - */ - const baseCost = priceEth(event.args.cost); - const premium = priceEth(0n); - const total = baseCost; + const baseCost = priceEth(event.args.baseCost); + const premium = priceEth(event.args.premium); + const total = addPrices(baseCost, premium); const pricing = { baseCost, premium, total, } satisfies RegistrarActionPricingAvailable; - /** - * Ethnames_LegacyEthRegistrarController does not implement referrals or - * emits a referrer in events. - */ const referral = { encodedReferrer: null, decodedReferrer: null, @@ -126,17 +118,20 @@ export default function () { ); /** - * Ethnames_WrappedEthRegistrarController Event Handlers + * NameRegistered (yes base cost, yes premium, yes referral) + * - UnwrappedEthRegistrarController */ - addOnchainEventListener( - namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController:NameRegistered"), + namespaceContract( + pluginName, + "Ethnames_RegistrarController:NameRegistered(string label, bytes32 indexed labelhash, address indexed owner, uint256 baseCost, uint256 premium, uint256 expires, bytes32 referrer)", + ), async ({ context, event }) => { const { id, args: { - // this field is the labelhash, not the label - label: labelHash, + // rename to labelHash + labelhash: labelHash, }, } = event; @@ -145,9 +140,6 @@ export default function () { const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; - /** - * Ethnames_WrappedEthRegistrarController implements premiums, and base cost. - */ const baseCost = priceEth(event.args.baseCost); const premium = priceEth(event.args.premium); const total = addPrices(baseCost, premium); @@ -157,14 +149,12 @@ export default function () { total, } satisfies RegistrarActionPricingAvailable; - /** - * Ethnames_WrappedEthRegistrarController does not implement referrals or - * emits a referrer in events. - */ + const encodedReferrer = event.args.referrer; + const decodedReferrer = decodeEncodedReferrer(encodedReferrer); const referral = { - encodedReferrer: null, - decodedReferrer: null, - } satisfies RegistrarActionReferralNotApplicable; + encodedReferrer, + decodedReferrer, + } satisfies RegistrarActionReferralAvailable; await handleRegistrarControllerEvent(context, { id, @@ -176,8 +166,16 @@ export default function () { }, ); + /** + * NameRenewed (yes base cost, no premium, no referral). + * - LegacyEthRegistrarController + * - WrappedEthRegistrarController + */ addOnchainEventListener( - namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController:NameRenewed"), + namespaceContract( + pluginName, + "Ethnames_RegistrarController:NameRenewed(string name, bytes32 indexed label, uint256 cost, uint256 expires)", + ), async ({ context, event }) => { const { id, @@ -192,11 +190,6 @@ export default function () { const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; - /** - * Ethnames_WrappedEthRegistrarController implements premiums, and base cost. - * - * Premium for renewals is always 0 anyway. - */ const baseCost = priceEth(event.args.cost); const premium = priceEth(0n); const total = baseCost; @@ -206,10 +199,6 @@ export default function () { total, } satisfies RegistrarActionPricingAvailable; - /** - * Ethnames_WrappedEthRegistrarController does not implement referrals or - * emits a referrer in events. - */ const referral = { encodedReferrer: null, decodedReferrer: null, @@ -226,11 +215,14 @@ export default function () { ); /** - * Ethnames_UnwrappedEthRegistrarController Event Handlers + * NameRenewed (yes base cost, no premium, yes referral) + * - UnwrappedEthRegistrarController: */ - addOnchainEventListener( - namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController:NameRegistered"), + namespaceContract( + pluginName, + "Ethnames_RegistrarController:NameRenewed(string label, bytes32 indexed labelhash, uint256 cost, uint256 expires, bytes32 referrer)", + ), async ({ context, event }) => { const { id, @@ -245,25 +237,17 @@ export default function () { const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; - /** - * Ethnames_UnwrappedEthRegistrarController implements premiums, and base cost. - */ - const baseCost = priceEth(event.args.baseCost); - const premium = priceEth(event.args.premium); - const total = addPrices(baseCost, premium); + const baseCost = priceEth(event.args.cost); + const premium = priceEth(0n); + const total = baseCost; const pricing = { baseCost, premium, total, } satisfies RegistrarActionPricingAvailable; - /** - * Ethnames_UnwrappedEthRegistrarController implements referrals and - * emits a referrer in events. - */ const encodedReferrer = event.args.referrer; const decodedReferrer = decodeEncodedReferrer(encodedReferrer); - const referral = { encodedReferrer, decodedReferrer, @@ -279,15 +263,16 @@ export default function () { }, ); + /** + * RenewalReferred (no base cost, no premium, yes referrer). + * - UniversalRegistrarRenewalWithReferrer + */ addOnchainEventListener( - namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController:NameRenewed"), + namespaceContract(pluginName, "Ethnames_RegistrarController:RenewalReferred"), async ({ context, event }) => { const { id, - args: { - // rename to labelHash - labelhash: labelHash, - }, + args: { labelHash }, } = event; const subregistryId = getThisAccountId(context, event); @@ -295,36 +280,16 @@ export default function () { const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; - /** - * Ethnames_UnwrappedEthRegistrarController implements premiums, and base cost. - * - * Premium for renewals is always 0 anyway. - */ - const baseCost = priceEth(event.args.cost); - const premium = priceEth(0n); - const total = baseCost; - const pricing = { - baseCost, - premium, - total, - } satisfies RegistrarActionPricingAvailable; - - /** - * Ethnames_UnwrappedEthRegistrarController implements referrals and - * emits a referrer in events. - */ const encodedReferrer = event.args.referrer; const decodedReferrer = decodeEncodedReferrer(encodedReferrer); - const referral = { encodedReferrer, decodedReferrer, } satisfies RegistrarActionReferralAvailable; - await handleRegistrarControllerEvent(context, { + await handleUniversalRegistrarRenewalEvent(context, { id, node, - pricing, referral, transactionHash, }); diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts deleted file mode 100644 index 5bd158e75b..0000000000 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { makeSubdomainNode } from "enssdk"; - -import { - decodeEncodedReferrer, - PluginName, - type RegistrarActionReferralAvailable, -} from "@ensnode/ensnode-sdk"; - -import { getThisAccountId } from "@/lib/get-this-account-id"; -import { addOnchainEventListener } from "@/lib/indexing-engines/ponder"; -import { getManagedName } from "@/lib/managed-names"; -import { namespaceContract } from "@/lib/plugin-helpers"; -import { handleUniversalRegistrarRenewalEvent } from "@/plugins/registrars/shared/lib/universal-registrar-renewal-with-referrer-events"; - -/** - * Registers event handlers with Ponder. - */ -export default function () { - const pluginName = PluginName.Registrars; - - addOnchainEventListener( - namespaceContract(pluginName, "Ethnames_UniversalRegistrarRenewalWithReferrer:RenewalReferred"), - async ({ context, event }) => { - const { - id, - args: { labelHash }, - } = event; - - const subregistryId = getThisAccountId(context, event); - const { node: managedNode } = getManagedName(subregistryId); - const node = makeSubdomainNode(labelHash, managedNode); - const transactionHash = event.transaction.hash; - - /** - * Ethnames_UniversalRegistrarRenewalWithReferrer implements referrals and - * emits a referrer in events. - */ - const encodedReferrer = event.args.referrer; - const decodedReferrer = decodeEncodedReferrer(encodedReferrer); - - const referral = { - encodedReferrer, - decodedReferrer, - } satisfies RegistrarActionReferralAvailable; - - await handleUniversalRegistrarRenewalEvent(context, { - id, - node, - referral, - transactionHash, - }); - }, - ); -} diff --git a/apps/ensindexer/src/plugins/registrars/event-handlers.ts b/apps/ensindexer/src/plugins/registrars/event-handlers.ts index d0c628cfba..a34ae61c8c 100644 --- a/apps/ensindexer/src/plugins/registrars/event-handlers.ts +++ b/apps/ensindexer/src/plugins/registrars/event-handlers.ts @@ -1,15 +1,13 @@ import attach_Basenames_Registrars from "./basenames/handlers/Basenames_Registrar"; import attach_Basenames_RegistrarControllers from "./basenames/handlers/Basenames_RegistrarController"; import attach_Ethnames_Registrars from "./ethnames/handlers/Ethnames_Registrar"; -import attach_Ethnames_RegistrarControllers from "./ethnames/handlers/Ethnames_RegistrarController"; -import attach_Ethnames_UniversalRegistrarRenewalWithReferrer from "./ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer"; +import attach_Ethnames_RegistrarController from "./ethnames/handlers/Ethnames_RegistrarController"; import attach_Lineanames_Registrars from "./lineanames/handlers/Lineanames_Registrar"; import attach_Lineanames_RegistrarControllers from "./lineanames/handlers/Lineanames_RegistrarController"; export default function () { attach_Ethnames_Registrars(); - attach_Ethnames_RegistrarControllers(); - attach_Ethnames_UniversalRegistrarRenewalWithReferrer(); + attach_Ethnames_RegistrarController(); attach_Basenames_Registrars(); attach_Basenames_RegistrarControllers(); diff --git a/apps/ensindexer/src/plugins/registrars/plugin.ts b/apps/ensindexer/src/plugins/registrars/plugin.ts index cc956ff802..dde5173d19 100644 --- a/apps/ensindexer/src/plugins/registrars/plugin.ts +++ b/apps/ensindexer/src/plugins/registrars/plugin.ts @@ -9,7 +9,7 @@ import { createConfig } from "ponder"; -import { DatasourceNames } from "@ensnode/datasources"; +import { AnyRegistrarControllerABI, DatasourceNames } from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; @@ -17,6 +17,8 @@ import { chainConfigForContract, chainsConnectionConfigForDatasources, getRequiredDatasources, + mergedChainConfigForContracts, + pickContracts, } from "@/lib/ponder-helpers"; const pluginName = PluginName.Registrars; @@ -60,37 +62,18 @@ export default createPlugin({ ////////////////////////////////// // Ethnames Registrar Controllers ////////////////////////////////// - [namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - ethnames.chain.id, - ethnames.contracts.LegacyEthRegistrarController, - ), - abi: ethnames.contracts.LegacyEthRegistrarController.abi, - }, - [namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - ethnames.chain.id, - ethnames.contracts.WrappedEthRegistrarController, - ), - abi: ethnames.contracts.WrappedEthRegistrarController.abi, - }, - [namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - ethnames.chain.id, - ethnames.contracts.UnwrappedEthRegistrarController, - ), - abi: ethnames.contracts.UnwrappedEthRegistrarController.abi, - }, - [namespaceContract(pluginName, "Ethnames_UniversalRegistrarRenewalWithReferrer")]: { - chain: chainConfigForContract( + [namespaceContract(pluginName, "Ethnames_RegistrarController")]: { + abi: AnyRegistrarControllerABI, + chain: mergedChainConfigForContracts( config.globalBlockrange, ethnames.chain.id, - ethnames.contracts.UniversalRegistrarRenewalWithReferrer, + pickContracts(ethnames.contracts, [ + "LegacyEthRegistrarController", + "WrappedEthRegistrarController", + "UnwrappedEthRegistrarController", + "UniversalRegistrarRenewalWithReferrer", + ]), ), - abi: ethnames.contracts.UniversalRegistrarRenewalWithReferrer.abi, }, /////////////////////// diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts index fe432f6cbc..8d4c405487 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts @@ -75,12 +75,17 @@ export default function () { }, ); - /////////////////////////////// - // LegacyEthRegistrarController - /////////////////////////////// - + /** + * NameRegistered (yes base cost, no premium, no referral) + * - LegacyEthRegistrarController + * + * `name`/`label` are misnamed onchain — re-map to ENSNode terminology. + */ addOnchainEventListener( - namespaceContract(pluginName, "LegacyEthRegistrarController:NameRegistered"), + namespaceContract( + pluginName, + "RegistrarController:NameRegistered(string name, bytes32 indexed label, address indexed owner, uint256 cost, uint256 expires)", + ), async ({ context, event }) => { await handleNameRegisteredByController({ context, @@ -88,7 +93,6 @@ export default function () { ...event, args: { ...event.args, - // LegacyEthRegistrarController incorrectly names its event arguments, so we re-map them here label: event.args.name, labelHash: event.args.label, }, @@ -97,40 +101,52 @@ export default function () { }, ); + /** + * NameRegistered (yes base cost, yes premium, no referral) + * - WrappedEthRegistrarController + * + * `name`/`label` are misnamed onchain — re-map to ENSNode terminology. + * `cost` = baseCost + premium. + */ addOnchainEventListener( - namespaceContract(pluginName, "LegacyEthRegistrarController:NameRenewed"), + namespaceContract( + pluginName, + "RegistrarController:NameRegistered(string name, bytes32 indexed label, address indexed owner, uint256 baseCost, uint256 premium, uint256 expires)", + ), async ({ context, event }) => { - await handleNameRenewedByController({ + await handleNameRegisteredByController({ context, event: { ...event, args: { - ...event.args, - // LegacyEthRegistrarController incorrectly names its event arguments, so we re-map them here label: event.args.name, labelHash: event.args.label, + cost: event.args.baseCost + event.args.premium, }, }, }); }, ); - //////////////////////////////// - // WrappedEthRegistrarController - //////////////////////////////// - + /** + * NameRegistered (yes base cost, yes premium, yes referral) + * - UnwrappedEthRegistrarController + * + * `cost` = baseCost + premium. + */ addOnchainEventListener( - namespaceContract(pluginName, "WrappedEthRegistrarController:NameRegistered"), + namespaceContract( + pluginName, + "RegistrarController:NameRegistered(string label, bytes32 indexed labelhash, address indexed owner, uint256 baseCost, uint256 premium, uint256 expires, bytes32 referrer)", + ), async ({ context, event }) => { await handleNameRegisteredByController({ context, event: { ...event, args: { - // WrappedEthRegistrarController incorrectly names its event arguments, so we re-map them here - label: event.args.name, - labelHash: event.args.label, - // the WrappedEthRegistrarController#NameRegistered uses baseCost + premium for full cost + label: event.args.label, + labelHash: event.args.labelhash, cost: event.args.baseCost + event.args.premium, }, }, @@ -138,8 +154,18 @@ export default function () { }, ); + /** + * NameRenewed (yes base cost, no premium, no referral) + * - LegacyEthRegistrarController + * - WrappedEthRegistrarController + * + * `name`/`label` are misnamed onchain — re-map to ENSNode terminology. + */ addOnchainEventListener( - namespaceContract(pluginName, "WrappedEthRegistrarController:NameRenewed"), + namespaceContract( + pluginName, + "RegistrarController:NameRenewed(string name, bytes32 indexed label, uint256 cost, uint256 expires)", + ), async ({ context, event }) => { await handleNameRenewedByController({ context, @@ -147,7 +173,6 @@ export default function () { ...event, args: { ...event.args, - // WrappedEthRegistrarController incorrectly names its event arguments, so we re-map them here label: event.args.name, labelHash: event.args.label, }, @@ -156,31 +181,15 @@ export default function () { }, ); - ////////////////////////////////// - // UnwrappedEthRegistrarController - ////////////////////////////////// - - addOnchainEventListener( - namespaceContract(pluginName, "UnwrappedEthRegistrarController:NameRegistered"), - async ({ context, event }) => { - await handleNameRegisteredByController({ - context, - event: { - ...event, - args: { - label: event.args.label, - // NOTE: remapping `labelhash` to `labelHash` to match ENSNode terminology - labelHash: event.args.labelhash, - // the UnwrappedEthRegistrarController#NameRegistered uses baseCost + premium for full cost - cost: event.args.baseCost + event.args.premium, - }, - }, - }); - }, - ); - + /** + * NameRenewed (yes base cost, no premium, yes referral) + * - UnwrappedEthRegistrarController + */ addOnchainEventListener( - namespaceContract(pluginName, "UnwrappedEthRegistrarController:NameRenewed"), + namespaceContract( + pluginName, + "RegistrarController:NameRenewed(string label, bytes32 indexed labelhash, uint256 cost, uint256 expires, bytes32 referrer)", + ), async ({ context, event }) => { await handleNameRenewedByController({ context, @@ -189,9 +198,7 @@ export default function () { args: { ...event.args, label: event.args.label, - // NOTE: remapping `labelhash` to `labelHash` to match ENSNode terminology labelHash: event.args.labelhash, - // UnwrappedEthRegistrarController#NameRenewed provides direct `cost` argument }, }, }); diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts index db35e3d41b..e0cb6eb84b 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/plugin.ts @@ -5,7 +5,7 @@ import * as ponder from "ponder"; -import { DatasourceNames } from "@ensnode/datasources"; +import { AnyRegistrarControllerABI, DatasourceNames } from "@ensnode/datasources"; import { PluginName } from "@ensnode/ensnode-sdk"; import { createPlugin, namespaceContract } from "@/lib/plugin-helpers"; @@ -13,6 +13,8 @@ import { chainConfigForContract, chainsConnectionConfigForDatasources, getRequiredDatasources, + mergedChainConfigForContracts, + pickContracts, } from "@/lib/ponder-helpers"; const pluginName = PluginName.Subgraph; @@ -51,29 +53,17 @@ export default createPlugin({ chain: chainConfigForContract(config.globalBlockrange, chain.id, contracts.BaseRegistrar), abi: contracts.BaseRegistrar.abi, }, - [namespaceContract(pluginName, "LegacyEthRegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - chain.id, - contracts.LegacyEthRegistrarController, - ), - abi: contracts.LegacyEthRegistrarController.abi, - }, - [namespaceContract(pluginName, "WrappedEthRegistrarController")]: { - chain: chainConfigForContract( - config.globalBlockrange, - chain.id, - contracts.WrappedEthRegistrarController, - ), - abi: contracts.WrappedEthRegistrarController.abi, - }, - [namespaceContract(pluginName, "UnwrappedEthRegistrarController")]: { - chain: chainConfigForContract( + [namespaceContract(pluginName, "RegistrarController")]: { + abi: AnyRegistrarControllerABI, + chain: mergedChainConfigForContracts( config.globalBlockrange, chain.id, - contracts.UnwrappedEthRegistrarController, + pickContracts(contracts, [ + "LegacyEthRegistrarController", + "WrappedEthRegistrarController", + "UnwrappedEthRegistrarController", + ]), ), - abi: contracts.UnwrappedEthRegistrarController.abi, }, [namespaceContract(pluginName, "NameWrapper")]: { chain: chainConfigForContract(config.globalBlockrange, chain.id, contracts.NameWrapper), diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/ens-test-env.ts index cdde9805c5..2f178b80b3 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -1,5 +1,3 @@ -import { zeroAddress } from "viem"; - import { EnhancedAccessControl } from "./abis/ensv2/EnhancedAccessControl"; import { ETHRegistrar } from "./abis/ensv2/ETHRegistrar"; import { Registry } from "./abis/ensv2/Registry"; @@ -9,7 +7,6 @@ import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; import { NameWrapper as root_NameWrapper } from "./abis/root/NameWrapper"; import { Registry as root_Registry } from "./abis/root/Registry"; -import { UniversalRegistrarRenewalWithReferrer as root_UniversalRegistrarRenewalWithReferrer } from "./abis/root/UniversalRegistrarRenewalWithReferrer"; import { UniversalResolverV1 } from "./abis/root/UniversalResolverV1"; import { UnwrappedEthRegistrarController as root_UnwrappedEthRegistrarController } from "./abis/root/UnwrappedEthRegistrarController"; import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } from "./abis/root/WrappedEthRegistrarController"; @@ -82,12 +79,6 @@ export default { address: "0x367761085bf3c12e5da2df99ac6e1a824612b8fb", startBlock: 0, }, - // NOTE: not in devnet, set to zeroAddress - UniversalRegistrarRenewalWithReferrer: { - abi: root_UniversalRegistrarRenewalWithReferrer, - address: zeroAddress, - startBlock: 0, - }, NameWrapper: { abi: root_NameWrapper, address: "0x5081a39b8a5f0e35a8d959395a630b68b74dd30f", diff --git a/packages/datasources/src/lib/AnyRegistrarControllerABI.ts b/packages/datasources/src/lib/AnyRegistrarControllerABI.ts index 3978508e03..8812c30ac9 100644 --- a/packages/datasources/src/lib/AnyRegistrarControllerABI.ts +++ b/packages/datasources/src/lib/AnyRegistrarControllerABI.ts @@ -5,6 +5,7 @@ import { RegistrarController } from "../abis/basenames/RegistrarController"; import { UpgradeableRegistrarController } from "../abis/basenames/UpgradeableRegistrarController"; import { EthRegistrarController } from "../abis/lineanames/EthRegistrarController"; import { LegacyEthRegistrarController } from "../abis/root/LegacyEthRegistrarController"; +import { UniversalRegistrarRenewalWithReferrer } from "../abis/root/UniversalRegistrarRenewalWithReferrer"; import { UnwrappedEthRegistrarController } from "../abis/root/UnwrappedEthRegistrarController"; import { WrappedEthRegistrarController } from "../abis/root/WrappedEthRegistrarController"; @@ -13,6 +14,7 @@ export const AnyRegistrarControllerABI = mergeAbis([ LegacyEthRegistrarController, WrappedEthRegistrarController, UnwrappedEthRegistrarController, + UniversalRegistrarRenewalWithReferrer, // basenames EarlyAccessRegistrarController, RegistrarController, diff --git a/packages/datasources/src/sepolia-v2.ts b/packages/datasources/src/sepolia-v2.ts index f3e51f13e5..79a943826d 100644 --- a/packages/datasources/src/sepolia-v2.ts +++ b/packages/datasources/src/sepolia-v2.ts @@ -1,5 +1,3 @@ -import { zeroAddress } from "viem"; - // ABIs for ENSv2 Datasource import { EnhancedAccessControl } from "./abis/ensv2/EnhancedAccessControl"; import { ETHRegistrar } from "./abis/ensv2/ETHRegistrar"; @@ -7,13 +5,10 @@ import { Registry } from "./abis/ensv2/Registry"; import { UniversalResolverV2 } from "./abis/ensv2/UniversalResolverV2"; // ABIs for ENSRoot Datasource import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; -import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; import { NameWrapper as root_NameWrapper } from "./abis/root/NameWrapper"; import { Registry as root_Registry } from "./abis/root/Registry"; -import { UniversalRegistrarRenewalWithReferrer as root_UniversalRegistrarRenewalWithReferrer } from "./abis/root/UniversalRegistrarRenewalWithReferrer"; import { UniversalResolverV1 } from "./abis/root/UniversalResolverV1"; import { UnwrappedEthRegistrarController as root_UnwrappedEthRegistrarController } from "./abis/root/UnwrappedEthRegistrarController"; -import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } from "./abis/root/WrappedEthRegistrarController"; // Shared ABIs import { StandaloneReverseRegistrar } from "./abis/shared/StandaloneReverseRegistrar"; import { sepoliaV2Chain } from "./lib/chains"; @@ -30,8 +25,9 @@ export default { /** * ENS Root contracts deployed on Sepolia for the ENSv1 + ENSv2 test deployment. * - * NOTE: `UniversalRegistrarRenewalWithReferrer` is a placeholder entry required by the typesystem - * due to the registrar plugin; it does not exist on Sepolia V2 and therefore uses the zero address. + * NOTE: `LegacyEthRegistrarController`, `WrappedEthRegistrarController`, and + * `UniversalRegistrarRenewalWithReferrer` are not part of this deployment and are therefore + * omitted; consumers of this datasource must treat them as optional. */ [DatasourceNames.ENSRoot]: { chain: sepoliaV2Chain, @@ -58,30 +54,12 @@ export default { address: "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85", startBlock: 3702731, }, - // NOTE: as per ENS Team, indexing this contract isn't relevant in sepolia-v2 namespace - LegacyEthRegistrarController: { - abi: root_LegacyEthRegistrarController, - address: zeroAddress, - startBlock: 0, - }, - // NOTE: as per ENS Team, indexing this contract isn't relevant in sepolia-v2 namespace - WrappedEthRegistrarController: { - abi: root_WrappedEthRegistrarController, - address: zeroAddress, - startBlock: 0, - }, // NOTE: named ETHRegistrarController in deployment UnwrappedEthRegistrarController: { abi: root_UnwrappedEthRegistrarController, address: "0xfb3ce5d01e0f33f41dbb39035db9745962f1f968", startBlock: 8579988, }, - // NOTE: not in deployment, set to zeroAddress - UniversalRegistrarRenewalWithReferrer: { - abi: root_UniversalRegistrarRenewalWithReferrer, - address: zeroAddress, - startBlock: 0, - }, NameWrapper: { abi: root_NameWrapper, address: "0x0635513f179d50a207757e05759cbd106d7dfce8", diff --git a/packages/ensnode-sdk/src/shared/config/indexed-blockranges.test.ts b/packages/ensnode-sdk/src/shared/config/indexed-blockranges.test.ts index 7aa96e09c8..908ab4cc63 100644 --- a/packages/ensnode-sdk/src/shared/config/indexed-blockranges.test.ts +++ b/packages/ensnode-sdk/src/shared/config/indexed-blockranges.test.ts @@ -1,5 +1,4 @@ import type { ChainId } from "enssdk"; -import { zeroAddress } from "viem"; import { afterEach, describe, expect, it, vi } from "vitest"; import * as datasources from "@ensnode/datasources"; @@ -156,39 +155,4 @@ describe("buildIndexedBlockranges()", () => { // Assert expect(result).toStrictEqual(new Map()); }); - - it("skips zero-address placeholder contracts", () => { - // Arrange - // Mirrors the sepolia-v2 shape where some plugin-required contracts are - // present only to satisfy the typesystem and carry address: zeroAddress, - // startBlock: 0. The merged blockrange should be derived from the real - // contracts on the same chain only, not be dragged down to startBlock 0. - const ensrootDatasourceConfig: unknown = { - chain: { id: 1 }, - contracts: { - registry: { address: "0x0000000000000000000000000000000000000001", startBlock: 100 }, - placeholder: { address: zeroAddress, startBlock: 0 }, - }, - }; - - const datasourcesByName: Partial< - Record> - > = { - [DatasourceNames.ENSRoot]: datasourceMock(ensrootDatasourceConfig), - }; - - maybeGetDatasourceMock.mockImplementation( - (_namespace, datasourceName) => datasourcesByName[datasourceName as DatasourceName], - ); - - const pluginsRequiredDatasourceNames = new Map([ - [PluginName.Subgraph, [DatasourceNames.ENSRoot]], - ]); - - // Act - const result = buildIndexedBlockranges(ENSNamespaceIds.Mainnet, pluginsRequiredDatasourceNames); - - // Assert - expect(result).toStrictEqual(new Map([[1, buildBlockNumberRange(100, undefined)]])); - }); }); diff --git a/packages/ensnode-sdk/src/shared/config/indexed-blockranges.ts b/packages/ensnode-sdk/src/shared/config/indexed-blockranges.ts index e22a3bed4c..3611d71d2f 100644 --- a/packages/ensnode-sdk/src/shared/config/indexed-blockranges.ts +++ b/packages/ensnode-sdk/src/shared/config/indexed-blockranges.ts @@ -1,5 +1,4 @@ import type { ChainId } from "enssdk"; -import { zeroAddress } from "viem"; import { type ContractConfig, @@ -38,12 +37,6 @@ export function buildIndexedBlockranges( const datasourceContracts = Object.values(datasource.contracts); for (const datasourceContract of datasourceContracts) { - // Skip placeholder contracts that exist only to satisfy the typesystem - // (e.g. cross-namespace registrar entries set to the zero address). They - // are not actually indexed by Ponder, so including their startBlock=0 - // would incorrectly drag the chain's indexed blockrange lower bound to 0. - if (datasourceContract.address === zeroAddress) continue; - const currentChainIndexedBlockrange = indexedBlockranges.get(datasourceChainId); const contractIndexedBlockrange = buildBlockNumberRange( diff --git a/packages/ensnode-sdk/src/shared/managed-names.ts b/packages/ensnode-sdk/src/shared/managed-names.ts index 215e176411..d39c28f4a7 100644 --- a/packages/ensnode-sdk/src/shared/managed-names.ts +++ b/packages/ensnode-sdk/src/shared/managed-names.ts @@ -101,20 +101,28 @@ const getContractsByManagedName = (namespace: ENSNamespaceId) => { registry: ensRootRegistry, contracts: [ getDatasourceContract(namespace, DatasourceNames.ENSRoot, "BaseRegistrar"), - getDatasourceContract(namespace, DatasourceNames.ENSRoot, "LegacyEthRegistrarController"), - getDatasourceContract(namespace, DatasourceNames.ENSRoot, "WrappedEthRegistrarController"), getDatasourceContract( namespace, DatasourceNames.ENSRoot, "UnwrappedEthRegistrarController", ), - getDatasourceContract( + maybeGetDatasourceContract( + namespace, + DatasourceNames.ENSRoot, + "LegacyEthRegistrarController", + ), + maybeGetDatasourceContract( + namespace, + DatasourceNames.ENSRoot, + "WrappedEthRegistrarController", + ), + maybeGetDatasourceContract( namespace, DatasourceNames.ENSRoot, "UniversalRegistrarRenewalWithReferrer", ), ethnamesNameWrapper, - ], + ].filter((c): c is AccountId => !!c), }, ...(basenamesRegistry && { "base.eth": { From d9758843e5632137810d13471e7df4340f066819 Mon Sep 17 00:00:00 2001 From: shrugs Date: Sun, 3 May 2026 16:09:47 -0500 Subject: [PATCH 2/4] fix: cleanup dos --- .changeset/eliminate-zero-address-placeholders.md | 10 +--------- .changeset/ensv2-registrar-controller-bug.md | 5 +++++ apps/ensindexer/src/lib/ponder-helpers.ts | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) create mode 100644 .changeset/ensv2-registrar-controller-bug.md diff --git a/.changeset/eliminate-zero-address-placeholders.md b/.changeset/eliminate-zero-address-placeholders.md index 42bcf8cf24..86f4f6f7a3 100644 --- a/.changeset/eliminate-zero-address-placeholders.md +++ b/.changeset/eliminate-zero-address-placeholders.md @@ -1,13 +1,5 @@ --- "@ensnode/datasources": patch -"ensindexer": patch -"@ensnode/ensnode-sdk": patch --- -Eliminate zero-address placeholder contracts; merge ENSRoot registrar controllers into a single Ponder contract entry per chain. - -- `@ensnode/datasources`: removed `LegacyEthRegistrarController`, `WrappedEthRegistrarController`, and `UniversalRegistrarRenewalWithReferrer` placeholder entries from the `sepolia-v2` namespace, and `UniversalRegistrarRenewalWithReferrer` from `ens-test-env`. These were typed-but-unindexable entries pinned at `address: zeroAddress, startBlock: 0`. `AnyRegistrarControllerABI` now also includes the `UniversalRegistrarRenewalWithReferrer` ABI so its `RenewalReferred` event participates in the merged controller dispatch. -- `ensindexer`: - - `registrars` and `subgraph` plugins: the four per-controller Ponder contract entries (`Legacy`, `Wrapped`, `Unwrapped`, `URRWR`) are replaced by a single `RegistrarController` entry per chain that uses the merged `AnyRegistrarControllerABI`. Controller addresses are combined into one chain entry via the new `mergedChainConfigForContracts` helper; controllers absent from the active namespace contribute no address. Handlers dispatch by long-form event signature, mirroring `apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts`. This removes the namespace-conditional contract-name typesystem problem entirely — the merged entry is always present because at least one controller (`UnwrappedEthRegistrarController`) exists in every namespace. - - `ensv2` plugin: the `RegistrarController` entry's per-chain configs are now built via `mergedChainConfigForContracts` instead of spreading multiple `chainConfigForContract(...)` results into the same `chain: {}` map. The previous spread pattern silently overwrote earlier entries for the same chain id (e.g. on mainnet only `UnwrappedEthRegistrarController` survived; Legacy and Wrapped were dropped). Fixes #2048. -- `@ensnode/ensnode-sdk`: reverted the zero-address skip in `buildIndexedBlockranges` from #2045 — the underlying placeholders no longer exist, so the workaround is unnecessary. `getContractsByManagedName` now treats the optional ENSRoot controllers as `maybeGetDatasourceContract` lookups and filters absent ones, matching the existing Basenames/Lineanames pattern. Together these fix the `historicalTotalBlocks` overshoot that produced `Block 14473749 not found` on sepolia-v2. +Removed `LegacyEthRegistrarController`, `WrappedEthRegistrarController`, and `UniversalRegistrarRenewalWithReferrer` placeholder entries from the `sepolia-v2` namespace, and `UniversalRegistrarRenewalWithReferrer` from `ens-test-env`. `AnyRegistrarControllerABI` now also includes the `UniversalRegistrarRenewalWithReferrer` ABI. diff --git a/.changeset/ensv2-registrar-controller-bug.md b/.changeset/ensv2-registrar-controller-bug.md new file mode 100644 index 0000000000..df3fb28612 --- /dev/null +++ b/.changeset/ensv2-registrar-controller-bug.md @@ -0,0 +1,5 @@ +--- +"ensindexer": patch +--- + +Fixed a bug in `ensv2` plugin where only `UnwrappedEthRegistrarController` was indexed and; `LegacyEthRegistrarController` and `WrappedEthRegistrarController` were silently dropped. Fixes #2048. diff --git a/apps/ensindexer/src/lib/ponder-helpers.ts b/apps/ensindexer/src/lib/ponder-helpers.ts index 662245c199..c3ee76f772 100644 --- a/apps/ensindexer/src/lib/ponder-helpers.ts +++ b/apps/ensindexer/src/lib/ponder-helpers.ts @@ -169,8 +169,8 @@ export function pickContracts( * Builds a single Ponder `chain: { [chainId]: { address, startBlock, endBlock } }` entry that * spans multiple contracts on the same chain (e.g. all of the .eth RegistrarControllers). * - * Use this when one Ponder contract entry should index events from multiple onchain contracts - * that share an ABI. + * Use this when one Ponder contract entry should index events from multiple contracts with addresses + * that share an ABI on the same chain. * * - `address` is the union of all defined contract addresses on this chain. * - `startBlock` is the earliest contract `startBlock`. From fe84f0612f1861b668b1fae6eeef97004fbc794b Mon Sep 17 00:00:00 2001 From: shrugs Date: Sun, 3 May 2026 16:16:08 -0500 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20bot=20notes=20(loop=201)=20=E2=80=94?= =?UTF-8?q?=20guard=20mergedChainConfigForContracts=20against=20empty=20in?= =?UTF-8?q?put?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/ensindexer/src/lib/ponder-helpers.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/ensindexer/src/lib/ponder-helpers.ts b/apps/ensindexer/src/lib/ponder-helpers.ts index c3ee76f772..7f5d4a8f64 100644 --- a/apps/ensindexer/src/lib/ponder-helpers.ts +++ b/apps/ensindexer/src/lib/ponder-helpers.ts @@ -184,6 +184,10 @@ export function mergedChainConfigForContracts( chainId: number, contracts: readonly ContractConfig[], ) { + if (contracts.length === 0) { + throw new Error("mergedChainConfigForContracts: contracts must not be empty"); + } + const addresses = contracts.flatMap((c) => Array.isArray(c.address) ? c.address : c.address ? [c.address] : [], ); From ff228ca05fcf8fce814d98e02597cbea1ed84e2d Mon Sep 17 00:00:00 2001 From: shrugs Date: Sun, 3 May 2026 16:27:04 -0500 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20bot=20notes=20(loop=202)=20=E2=80=94?= =?UTF-8?q?=20undefined=20address=20for=20factory-mode;=20changeset=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/ensv2-registrar-controller-bug.md | 2 +- apps/ensindexer/src/lib/ponder-helpers.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.changeset/ensv2-registrar-controller-bug.md b/.changeset/ensv2-registrar-controller-bug.md index df3fb28612..085f5b1f0b 100644 --- a/.changeset/ensv2-registrar-controller-bug.md +++ b/.changeset/ensv2-registrar-controller-bug.md @@ -2,4 +2,4 @@ "ensindexer": patch --- -Fixed a bug in `ensv2` plugin where only `UnwrappedEthRegistrarController` was indexed and; `LegacyEthRegistrarController` and `WrappedEthRegistrarController` were silently dropped. Fixes #2048. +Fixed a bug in `ensv2` plugin where only `UnwrappedEthRegistrarController` was indexed; `LegacyEthRegistrarController` and `WrappedEthRegistrarController` were silently dropped. Fixes #2048. diff --git a/apps/ensindexer/src/lib/ponder-helpers.ts b/apps/ensindexer/src/lib/ponder-helpers.ts index 7f5d4a8f64..978c20a15b 100644 --- a/apps/ensindexer/src/lib/ponder-helpers.ts +++ b/apps/ensindexer/src/lib/ponder-helpers.ts @@ -209,7 +209,10 @@ export function mergedChainConfigForContracts( return { [chainId.toString()]: { - address: addresses, + // when no contract supplies an address, leave `address` undefined so Ponder treats this as + // factory-mode ("index any address matching the ABI") rather than an explicit empty list, + // which Ponder treats as "index nothing". + address: addresses.length > 0 ? addresses : undefined, startBlock, endBlock, },