From 92fcddefa9af11d3a529cc97831b8aeab4fba741 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 23 Dec 2025 13:27:17 -0600 Subject: [PATCH 1/5] refactor: DRY managed names concept and standardize usage --- .../registrar-lib.ts => managed-names.ts} | 52 ++++++--- apps/ensindexer/src/lib/types.ts | 25 ----- .../ensv2/handlers/ensv1/BaseRegistrar.ts | 14 +-- .../ensv2/handlers/ensv1/NameWrapper.ts | 14 +-- .../handlers/ensv1/RegistrarController.ts | 6 +- .../basenames/handlers/Basenames_Registrar.ts | 38 +++---- .../handlers/Basenames_RegistrarController.ts | 84 +++++++++----- .../basenames/lib/registrar-helpers.ts | 32 ------ .../ethnames/handlers/Ethnames_Registrar.ts | 31 +++--- .../handlers/Ethnames_RegistrarController.ts | 104 ++++++++++++------ ...s_UniversalRegistrarRenewalWithReferrer.ts | 26 ++--- .../ethnames/lib/registrar-helpers.ts | 31 ------ .../handlers/Lineanames_Registrar.ts | 31 +++--- .../Lineanames_RegistrarController.ts | 70 ++++++++---- .../lineanames/lib/registrar-helpers.ts | 32 ------ .../plugins/basenames/handlers/Registrar.ts | 23 +--- .../basenames/lib/registrar-helpers.ts | 24 ---- .../lineanames/handlers/NameWrapper.ts | 10 +- .../plugins/lineanames/handlers/Registrar.ts | 23 +--- .../lineanames/lib/registrar-helpers.ts | 24 ---- .../plugins/subgraph/handlers/NameWrapper.ts | 5 +- .../plugins/subgraph/handlers/Registrar.ts | 20 +--- .../subgraph/shared-handlers/NameWrapper.ts | 17 +-- .../subgraph/shared-handlers/Registrar.ts | 68 ++++++------ .../docs/docs/concepts/what-is-ensnode.mdx | 2 +- 25 files changed, 331 insertions(+), 475 deletions(-) rename apps/ensindexer/src/lib/{ensv2/registrar-lib.ts => managed-names.ts} (52%) delete mode 100644 apps/ensindexer/src/lib/types.ts delete mode 100644 apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts delete mode 100644 apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts delete mode 100644 apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts delete mode 100644 apps/ensindexer/src/plugins/subgraph/plugins/basenames/lib/registrar-helpers.ts delete mode 100644 apps/ensindexer/src/plugins/subgraph/plugins/lineanames/lib/registrar-helpers.ts diff --git a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts b/apps/ensindexer/src/lib/managed-names.ts similarity index 52% rename from apps/ensindexer/src/lib/ensv2/registrar-lib.ts rename to apps/ensindexer/src/lib/managed-names.ts index 62e78a075..ace575f26 100644 --- a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts +++ b/apps/ensindexer/src/lib/managed-names.ts @@ -12,6 +12,26 @@ import { uint256ToHex32, } from "@ensnode/ensnode-sdk"; +import { toJson } from "@/lib/json-stringify-with-bigints"; + +/** + * Many Registrar contracts within the ENSv1 Ecosystem are relative to a parent Name. For example, + * the .eth BaseRegistrar (and RegistrarConrollers) manage direct subnames of .eth. As such, they + * operate on relative Labels, not fully qualified Names. We must know the parent name whose subnames + * they manage in order to index them correctly. + * + * Because we use shared indexing logic for each instance of these contracts (BaseRegistrar, + * RegistrarControllers, NameWrapper), the concept of "which name is this contract operating in the + * context of" must be generalizable: this is the Registrar Managed Name. + * + * Concretely, a .eth RegistrarController will emit a LabelHash indicating a new Registration, but + * correlating that LabelHash with the NameHash of the Name requires knowing the NameHash of the + * Registrar's Managed Name ('eth' in this case). + * + * The NameWrapper contracts are relevant here as well because they include specialized logic for + * wrapping direct subnames of these Registrar Managed Names. + */ + const ethnamesNameWrapper = getDatasourceContract( config.namespace, DatasourceNames.ENSRoot, @@ -25,9 +45,9 @@ const lineanamesNameWrapper = maybeGetDatasourceContract( ); /** - * Mapping of RegistrarManagedName to its related Registrar and Registrar-adjacent contracts. + * Mapping of a Managed Name to contracts that operate in the context of said Name. */ -const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { +const CONTRACTS_BY_MANAGED_NAME: Record = { eth: [ getDatasourceContract( config.namespace, // @@ -85,10 +105,9 @@ const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { }; /** - * Certain RegistrarManagedNames are different depending on the ENSNamespace — this encodes that - * relationship. + * Certain Managed Names are different depending on the ENSNamespace — this encodes that relationship. */ -const RMN_NAMESPACE_OVERRIDE: Partial>> = { +const MANAGED_NAME_BY_NAMESPACE: Partial>> = { sepolia: { "base.eth": "basetest.eth", "linea.eth": "linea-sepolia.eth", @@ -96,20 +115,20 @@ const RMN_NAMESPACE_OVERRIDE: Partial> }; /** - * Given a `contract`, identify its RegistrarManagedName. + * Given a `contract`, identify its ManagedName. */ -export const getRegistrarManagedName = (contract: AccountId) => { - for (const [managedName, contracts] of Object.entries(REGISTRAR_CONTRACTS_BY_MANAGED_NAME)) { +export const getManagedName = (contract: AccountId) => { + for (const [managedName, contracts] of Object.entries(CONTRACTS_BY_MANAGED_NAME)) { const isAnyOfTheContracts = contracts.some((_contract) => accountIdEqual(_contract, contract)); if (isAnyOfTheContracts) { + // override the default Managed Name with namespace-specific version if available const namespaceSpecificManagedName = - RMN_NAMESPACE_OVERRIDE[config.namespace]?.[managedName] ?? managedName; - // override the rmn with namespace-specific version if available + MANAGED_NAME_BY_NAMESPACE[config.namespace]?.[managedName] ?? managedName; return namespaceSpecificManagedName as InterpretedName; } } - throw new Error("never"); + throw new Error(`The following contract ${toJson(contract)} does not map to a Managed Name.`); }; /** @@ -122,9 +141,12 @@ export function isNameWrapper(contract: AccountId) { } /** - * BaseRegistrar-derived Registrars register direct subnames of a RegistrarManagedName. As such, the - * tokens issued by them are keyed by the direct subname's label's labelHash. + * BaseRegistrar-derived Registrars register direct subnames of a Managed Name. As such, the + * tokens issued by them are keyed by the direct subname's label's labelHash. This is identical for + * Basenames and Lineanames contracts as well. * - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 + * @see https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 + * @see https://github.com/base/basenames/blob/1b5c1ad/src/L2/RegistrarController.sol#L488 + * @see https://github.com/Consensys/linea-ens/blob/3a4f02f/packages/linea-ens-contracts/contracts/ethregistrar/ETHRegistrarController.sol#L447 */ -export const registrarTokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); +export const tokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); diff --git a/apps/ensindexer/src/lib/types.ts b/apps/ensindexer/src/lib/types.ts deleted file mode 100644 index 8df8cb7ec..000000000 --- a/apps/ensindexer/src/lib/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Name } from "@ensnode/ensnode-sdk"; - -/** - * RegistrarManagedName is an explicit type representing this concept within the shared handlers: - * "the parent name a Registrar contract registers subnames of" - * - * ENSIndexer uses "shared handlers" for common indexing logic across plugins. While not suitable - * for all theoretical ENS datasources, they work for current ones, particularly those that re-use the - * original ENS contracts (i.e. Basenames, Lineanames). When indexing onchain events, these handlers - * sometimes need context about parent ENS names of indexed subnames, which is what RegistrarManagedName - * provides. - * - * ex: .eth for the ETH Registry - * ex: .base.eth for Basenames Registry - * ex: .linea.eth for the Lineanames Registry - * - * Currently, the relationship between a plugin and a RegistrarManagedName is simplified to be 1:1. - * In the future, we plan to enhance this data model to support indexing any number of Registrars - * in a single plugin, which will be important for supporting 3DNS and other data sources. - * - * Additionally, our current implementation assumes data sources will share common indexing logic - * (via our shared registrar indexing handlers). We will be working to support more expressive - * or custom cases in the future, which will be necessary for 3DNS and other specialized integrations. - */ -export type RegistrarManagedName = Name; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index 6d0e4774f..3f3aca5f0 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -15,7 +15,6 @@ import { import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { materializeENSv1DomainEffectiveOwner } from "@/lib/ensv2/domain-db-helpers"; -import { getRegistrarManagedName, registrarTokenIdToLabelHash } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, getLatestRenewal, @@ -24,6 +23,7 @@ import { } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { toJson } from "@/lib/json-stringify-with-bigints"; +import { getManagedName, tokenIdToLabelHash } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -70,9 +70,9 @@ export default function () { // // in all such cases, a Registration is expected and we can conditionally materialize Domain owner - const labelHash = registrarTokenIdToLabelHash(tokenId); + const labelHash = tokenIdToLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(registrar)); + const managedNode = namehash(getManagedName(registrar)); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -101,9 +101,9 @@ export default function () { const { id: tokenId, owner, expires: expiry } = event.args; const registrant = owner; - const labelHash = registrarTokenIdToLabelHash(tokenId); + const labelHash = tokenIdToLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(registrar)); + const managedNode = namehash(getManagedName(registrar)); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -159,9 +159,9 @@ export default function () { }) => { const { id: tokenId, expires: expiry } = event.args; - const labelHash = registrarTokenIdToLabelHash(tokenId); + const labelHash = tokenIdToLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(registrar)); + const managedNode = namehash(getManagedName(registrar)); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts index 9e33eae2f..5cf495574 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -25,7 +25,6 @@ import { import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { materializeENSv1DomainEffectiveOwner } from "@/lib/ensv2/domain-db-helpers"; import { ensureLabel } from "@/lib/ensv2/label-db-helpers"; -import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, getLatestRenewal, @@ -34,6 +33,7 @@ import { } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { toJson } from "@/lib/json-stringify-with-bigints"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -64,7 +64,7 @@ const interpretExpiry = (expiry: bigint): bigint | null => (expiry === 0n ? null // .eth 2LDs always have PARENT_CANNOT_CONTROL set ('burned'), they cannot be transferred during grace period -const isDirectSubnameOfRegistrarManagedName = ( +const isDirectSubnameOfManagedName = ( managedNode: Node, name: DNSEncodedLiteralName, node: Node, @@ -78,7 +78,7 @@ const isDirectSubnameOfRegistrarManagedName = ( } catch { // must be decodable throw new Error( - `Invariant(isSubnameOfRegistrarManagedName): NameWrapper emitted DNSEncodedNames for direct-subnames-of-registrar-managed-names MUST be decodable`, + `Invariant(isDirectSubnameOfManagedName): NameWrapper emitted DNSEncodedNames for direct-subnames-of-managed-names MUST be decodable`, ); } @@ -187,12 +187,12 @@ export default function () { // handle wraps of direct-subname-of-registrar-managed-names if (registration && !isFullyExpired && registration.type === "BaseRegistrar") { - const managedNode = namehash(getRegistrarManagedName(getThisAccountId(context, event))); + const managedNode = namehash(getManagedName(getThisAccountId(context, event))); - // Invariant: Emitted name is a direct subname of the RegistrarManagedName - if (!isDirectSubnameOfRegistrarManagedName(managedNode, name, node)) { + // Invariant: Emitted name is a direct subname of the Managed Name + if (!isDirectSubnameOfManagedName(managedNode, name, node)) { throw new Error( - `Invariant(NameWrapper:NameWrapped): An unexpired BaseRegistrar Registration was found, but the name in question is NOT a direct subname of this NameWrapper's BaseRegistrar's RegistrarManagedName — wtf?`, + `Invariant(NameWrapper:NameWrapped): An unexpired BaseRegistrar Registration was found, but the name in question is NOT a direct subname of this NameWrapper's BaseRegistrar's Managed Name — wtf?`, ); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts index 9c68c6b2e..19b3dfbc0 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts @@ -15,10 +15,10 @@ import { } from "@ensnode/ensnode-sdk"; import { ensureLabel, ensureUnknownLabel } from "@/lib/ensv2/label-db-helpers"; -import { getRegistrarManagedName } from "@/lib/ensv2/registrar-lib"; import { getLatestRegistration, getLatestRenewal } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { toJson } from "@/lib/json-stringify-with-bigints"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -49,7 +49,7 @@ export default function () { } const controller = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(controller)); + const managedNode = namehash(getManagedName(controller)); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -91,7 +91,7 @@ export default function () { const label = _label as LiteralLabel; const controller = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(controller)); + const managedNode = namehash(getManagedName(controller)); const labelHash = labelhash(label); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); diff --git a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts index deebd7c44..724934c07 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts @@ -1,18 +1,16 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; import { namehash } from "viem/ens"; -import { DatasourceNames } from "@ensnode/datasources"; import { type BlockRef, bigIntToNumber, - getDatasourceContract, makeSubdomainNode, PluginName, type Subregistry, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName, tokenIdToLabelHash } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { @@ -20,32 +18,25 @@ import { handleRegistrarEventRenewal, } from "../../shared/lib/registrar-events"; import { upsertSubregistry } from "../../shared/lib/subregistry"; -import { getRegistrarManagedName, tokenIdToLabelHash } from "../lib/registrar-helpers"; /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.Basenames, - "BaseRegistrar", - ); - const subregistry = { - subregistryId, - node: parentNode, - } satisfies Subregistry; // support NameRegisteredWithRecord for BaseRegistrar as it used by Base's RegistrarControllers ponder.on( namespaceContract(pluginName, "Basenames_BaseRegistrar:NameRegisteredWithRecord"), async ({ context, event }) => { const id = event.id; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { @@ -72,8 +63,13 @@ export default function () { namespaceContract(pluginName, "Basenames_BaseRegistrar:NameRegistered"), async ({ context, event }) => { const id = event.id; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { @@ -100,8 +96,12 @@ export default function () { namespaceContract(pluginName, "Basenames_BaseRegistrar:NameRenewed"), async ({ context, event }) => { const id = event.id; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { diff --git a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts index 1d303d7aa..619d8906f 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts @@ -1,34 +1,24 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; import { namehash } from "viem/ens"; -import { DatasourceNames } from "@ensnode/datasources"; import { - getDatasourceContract, makeSubdomainNode, PluginName, type RegistrarActionPricingUnknown, type RegistrarActionReferralNotApplicable, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { handleRegistrarControllerEvent } from "../../shared/lib/registrar-controller-events"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.Basenames, - "BaseRegistrar", - ); /** * No Registrar Controller for Basenames implements premiums or @@ -59,9 +49,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Basenames_EARegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -82,9 +80,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Basenames_RegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -101,9 +107,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Basenames_RegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -124,9 +138,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Basenames_UpgradeableRegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -143,9 +165,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Basenames_UpgradeableRegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { diff --git a/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts deleted file mode 100644 index e0a4d3b39..000000000 --- a/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ENSNamespaceId } from "@ensnode/datasources"; -import { - getBasenamesSubregistryManagedName, - type LabelHash, - uint256ToHex32, -} from "@ensnode/ensnode-sdk"; - -import type { RegistrarManagedName } from "@/lib/types"; - -/** - * When direct subnames of Basenames are registered through - * the Basenames RegistrarController contract, - * an ERC721 NFT is minted that tokenizes ownership of the registration. - * The minted NFT will be assigned a unique tokenId represented as - * uint256(labelhash(label)) where label is the direct subname of - * the Basename that was registered. - * https://github.com/base/basenames/blob/1b5c1ad/src/L2/RegistrarController.sol#L488 - */ -export function tokenIdToLabelHash(tokenId: bigint): LabelHash { - return uint256ToHex32(tokenId); -} - -/** - * Get registrar managed name for `basenames` subregistry for selected ENS namespace. - * - * @param namespaceId - * @returns registrar managed name - * @throws an error when no registrar managed name could be returned - */ -export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarManagedName { - return getBasenamesSubregistryManagedName(namespaceId); -} diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts index c8524e12d..5bfcd8d24 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts @@ -1,18 +1,16 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; import { namehash } from "viem/ens"; -import { DatasourceNames } from "@ensnode/datasources"; import { type BlockRef, bigIntToNumber, - getDatasourceContract, makeSubdomainNode, PluginName, type Subregistry, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName, tokenIdToLabelHash } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { @@ -20,31 +18,24 @@ import { handleRegistrarEventRenewal, } from "../../shared/lib/registrar-events"; import { upsertSubregistry } from "../../shared/lib/subregistry"; -import { getRegistrarManagedName, tokenIdToLabelHash } from "../lib/registrar-helpers"; /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "BaseRegistrar", - ); - const subregistry = { - subregistryId, - node: parentNode, - } satisfies Subregistry; ponder.on( namespaceContract(pluginName, "Ethnames_BaseRegistrar:NameRegistered"), async ({ context, event }) => { const id = event.id; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { @@ -71,8 +62,12 @@ export default function () { namespaceContract(pluginName, "Ethnames_BaseRegistrar:NameRenewed"), async ({ context, event }) => { const id = event.id; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { 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 4e6864f9c..75691a16e 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts @@ -1,13 +1,9 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; import { namehash } from "viem"; -import { DatasourceNames } from "@ensnode/datasources"; import { addPrices, decodeEncodedReferrer, - getDatasourceContract, makeSubdomainNode, PluginName, priceEth, @@ -16,23 +12,17 @@ import { type RegistrarActionReferralNotApplicable, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { handleRegistrarControllerEvent } from "../../shared/lib/registrar-controller-events"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "BaseRegistrar", - ); /** * Ethnames_LegacyEthRegistrarController Event Handlers @@ -41,9 +31,18 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); + const transactionHash = event.transaction.hash; /** * Ethnames_LegacyEthRegistrarController does not implement premiums, @@ -67,8 +66,6 @@ export default function () { decodedReferrer: null, } satisfies RegistrarActionReferralNotApplicable; - const transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, { id, subregistryId, @@ -83,9 +80,18 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); + const transactionHash = event.transaction.hash; /** * Ethnames_LegacyEthRegistrarController does not implement premiums, @@ -111,8 +117,6 @@ export default function () { decodedReferrer: null, } satisfies RegistrarActionReferralNotApplicable; - const transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, { id, subregistryId, @@ -131,9 +135,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -171,9 +183,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -217,9 +237,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.labelhash; - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // rename to labelHash + labelhash: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -260,9 +288,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.labelhash; - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // rename to labelHash + labelhash: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts index d266b3856..4a39ed565 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts @@ -1,41 +1,35 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; import { namehash } from "viem"; -import { DatasourceNames } from "@ensnode/datasources"; import { decodeEncodedReferrer, - getDatasourceContract, makeSubdomainNode, PluginName, type RegistrarActionReferralAvailable, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +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"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; - /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "BaseRegistrar", - ); ponder.on( namespaceContract(pluginName, "Ethnames_UniversalRegistrarRenewalWithReferrer:RenewalReferred"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.labelHash; - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { labelHash }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts deleted file mode 100644 index e91d6ca92..000000000 --- a/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { ENSNamespaceId } from "@ensnode/datasources"; -import { - getEthnamesSubregistryManagedName, - type LabelHash, - uint256ToHex32, -} from "@ensnode/ensnode-sdk"; - -import type { RegistrarManagedName } from "@/lib/types"; - -/** - * When direct subnames of Ethnames are registered through - * the Ethnames ETHRegistrarController contract, - * an ERC721 NFT is minted that tokenizes ownership of the registration. - * The minted NFT will be assigned a unique tokenId which is - * uint256(labelhash(label)) where label is the direct subname of - * the Ethname that was registered. - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 - */ -export function tokenIdToLabelHash(tokenId: bigint): LabelHash { - return uint256ToHex32(tokenId); -} - -/** - * Get the registrar managed name for the Ethnames subregistry for the selected ENS namespace. - * - * @param namespaceId - * @returns registrar managed name - */ -export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarManagedName { - return getEthnamesSubregistryManagedName(namespaceId); -} diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts index fc99512c7..1af7e1018 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts @@ -1,18 +1,16 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; import { namehash } from "viem/ens"; -import { DatasourceNames } from "@ensnode/datasources"; import { type BlockRef, bigIntToNumber, - getDatasourceContract, makeSubdomainNode, PluginName, type Subregistry, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName, tokenIdToLabelHash } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { @@ -20,31 +18,24 @@ import { handleRegistrarEventRenewal, } from "../../shared/lib/registrar-events"; import { upsertSubregistry } from "../../shared/lib/subregistry"; -import { getRegistrarManagedName, tokenIdToLabelHash } from "../lib/registrar-helpers"; /** * Registers event handlers with Ponder. */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.Lineanames, - "BaseRegistrar", - ); - const subregistry = { - subregistryId, - node: parentNode, - } satisfies Subregistry; ponder.on( namespaceContract(pluginName, "Lineanames_BaseRegistrar:NameRegistered"), async ({ context, event }) => { const id = event.id; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { @@ -71,8 +62,12 @@ export default function () { namespaceContract(pluginName, "Lineanames_BaseRegistrar:NameRenewed"), async ({ context, event }) => { const id = event.id; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); const block = { diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts index 0175c7c92..413768ff6 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts @@ -1,12 +1,8 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; import { namehash } from "viem/ens"; -import { DatasourceNames } from "@ensnode/datasources"; import { addPrices, - getDatasourceContract, makeSubdomainNode, PluginName, priceEth, @@ -14,9 +10,10 @@ import { type RegistrarActionReferralNotApplicable, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; -import { getRegistrarManagedName } from "../../lineanames/lib/registrar-helpers"; import { handleRegistrarControllerEvent } from "../../shared/lib/registrar-controller-events"; /** @@ -24,13 +21,6 @@ import { handleRegistrarControllerEvent } from "../../shared/lib/registrar-contr */ export default function () { const pluginName = PluginName.Registrars; - const parentNode = namehash(getRegistrarManagedName(config.namespace)); - - const subregistryId = getDatasourceContract( - config.namespace, - DatasourceNames.Lineanames, - "BaseRegistrar", - ); /** * No Registrar Controller for Lineanames implements referrals or @@ -48,9 +38,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Lineanames_EthRegistrarController:OwnerNameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -78,9 +76,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Lineanames_EthRegistrarController:PohNameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -108,9 +114,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Lineanames_EthRegistrarController:NameRegistered"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; const baseCost = priceEth(event.args.baseCost); @@ -136,9 +150,17 @@ export default function () { ponder.on( namespaceContract(pluginName, "Lineanames_EthRegistrarController:NameRenewed"), async ({ context, event }) => { - const id = event.id; - const labelHash = event.args.label; // this field is the labelhash, not the label - const node = makeSubdomainNode(labelHash, parentNode); + const { + id, + args: { + // this field is the labelhash, not the label + label: labelHash, + }, + } = event; + + const subregistryId = getThisAccountId(context, event); + const managedNode = namehash(getManagedName(subregistryId)); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; const baseCost = priceEth(event.args.cost); diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts deleted file mode 100644 index 29d73486a..000000000 --- a/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ENSNamespaceId } from "@ensnode/datasources"; -import { - getLineanamesSubregistryManagedName, - type LabelHash, - uint256ToHex32, -} from "@ensnode/ensnode-sdk"; - -import type { RegistrarManagedName } from "@/lib/types"; - -/** - * When direct subnames of Lineanames are registered through - * the Lineanames ETHRegistrarController contract, - * an ERC721 NFT is minted that tokenizes ownership of the registration. - * The minted NFT will be assigned a unique tokenId represented as - * uint256(labelhash(label)) where label is the direct subname of - * Lineanames that was registered. - * https://github.com/Consensys/linea-ens/blob/3a4f02f/packages/linea-ens-contracts/contracts/ethregistrar/ETHRegistrarController.sol#L447 - */ -export function tokenIdToLabelHash(tokenId: bigint): LabelHash { - return uint256ToHex32(tokenId); -} - -/** - * Get registrar managed name for `lineanames` subregistry for selected ENS namespace. - * - * @param namespaceId - * @returns registrar managed name - * @throws an error when no registrar managed name could be returned - */ -export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarManagedName { - return getLineanamesSubregistryManagedName(namespaceId); -} diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts index 37f647197..45e7f896d 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts @@ -1,23 +1,11 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { type LabelHash, PluginName, uint256ToHex32 } from "@ensnode/ensnode-sdk"; +import { PluginName } from "@ensnode/ensnode-sdk"; +import { tokenIdToLabelHash } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { makeRegistrarHandlers } from "@/plugins/subgraph/shared-handlers/Registrar"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; - -/** - * When direct subnames of base.eth are registered through the base.eth RegistrarController contract - * on Base, an ERC721 NFT is minted that tokenizes ownership of the registration. The minted NFT will be - * assigned a unique tokenId represented as uint256(labelhash(label)) where label is the direct - * subname of base.eth that was registered. - * https://github.com/base/basenames/blob/1b5c1ad/src/L2/RegistrarController.sol#L488 - */ -const tokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); - /** * Registers event handlers with Ponder. */ @@ -30,12 +18,7 @@ export default function () { handleNameRenewedByController, handleNameRenewed, handleNameTransferred, - } = makeRegistrarHandlers({ - pluginName, - // the shared Registrar handlers in this plugin index direct subnames of - // the name returned from `getRegistrarManagedName` function call - registrarManagedName: getRegistrarManagedName(config.namespace), - }); + } = makeRegistrarHandlers({ pluginName }); // support NameRegisteredWithRecord for BaseRegistrar as it used by Base's RegistrarControllers ponder.on( diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/lib/registrar-helpers.ts deleted file mode 100644 index 74d74988c..000000000 --- a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/lib/registrar-helpers.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { ENSNamespaceId } from "@ensnode/datasources"; - -import type { RegistrarManagedName } from "@/lib/types"; - -/** - * Get registrar managed name for `basenames` plugin for selected ENS namespace. - * - * @param namespaceId - * @param pluginName - * @returns registrar managed name - * @throws an error when no registrar managed name could be returned - */ -export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarManagedName { - switch (namespaceId) { - case "mainnet": - return "base.eth"; - case "sepolia": - return "basetest.eth"; - case "ens-test-env": - throw new Error( - `No registrar managed name is known for the Basenames plugin within the "${namespaceId}" namespace.`, - ); - } -} diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/NameWrapper.ts index 8ae71b514..ac1ec5f38 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/NameWrapper.ts @@ -1,5 +1,3 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; import { PluginName } from "@ensnode/ensnode-sdk"; @@ -7,8 +5,6 @@ import { PluginName } from "@ensnode/ensnode-sdk"; import { namespaceContract } from "@/lib/plugin-helpers"; import { makeNameWrapperHandlers } from "@/plugins/subgraph/shared-handlers/NameWrapper"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; - /** * Registers event handlers with Ponder. */ @@ -22,11 +18,7 @@ export default function () { handleExpiryExtended, handleTransferSingle, handleTransferBatch, - } = makeNameWrapperHandlers({ - // the shared Registrar handlers in this plugin index direct subnames of - // the name returned from `getRegistrarManagedName` function call - registrarManagedName: getRegistrarManagedName(config.namespace), - }); + } = makeNameWrapperHandlers(); ponder.on(namespaceContract(pluginName, "NameWrapper:NameWrapped"), handleNameWrapped); ponder.on(namespaceContract(pluginName, "NameWrapper:NameUnwrapped"), handleNameUnwrapped); diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts index a80682d22..b48405cd2 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts @@ -1,23 +1,11 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { type LabelHash, PluginName, uint256ToHex32 } from "@ensnode/ensnode-sdk"; +import { PluginName } from "@ensnode/ensnode-sdk"; +import { tokenIdToLabelHash } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { makeRegistrarHandlers } from "@/plugins/subgraph/shared-handlers/Registrar"; -import { getRegistrarManagedName } from "../lib/registrar-helpers"; - -/** - * When direct subnames of linea.eth are registered through the linea.eth ETHRegistrarController - * contract on Linea, an ERC721 NFT is minted that tokenizes ownership of the registration. The minted NFT - * will be assigned a unique tokenId represented as uint256(labelhash(label)) where label is the - * direct subname of linea.eth that was registered. - * https://github.com/Consensys/linea-ens/blob/3a4f02f/packages/linea-ens-contracts/contracts/ethregistrar/ETHRegistrarController.sol#L447 - */ -const tokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); - /** * Registers event handlers with Ponder. */ @@ -30,12 +18,7 @@ export default function () { handleNameRenewedByController, handleNameRenewed, handleNameTransferred, - } = makeRegistrarHandlers({ - pluginName, - // the shared Registrar handlers in this plugin index direct subnames of - // the name returned from `getRegistrarManagedName` function call - registrarManagedName: getRegistrarManagedName(config.namespace), - }); + } = makeRegistrarHandlers({ pluginName }); ponder.on( namespaceContract(pluginName, "BaseRegistrar:NameRegistered"), diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/lib/registrar-helpers.ts deleted file mode 100644 index 6a99aed2f..000000000 --- a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/lib/registrar-helpers.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { ENSNamespaceId } from "@ensnode/datasources"; - -import type { RegistrarManagedName } from "@/lib/types"; - -/** - * Get registrar managed name for `lineanames` plugin for selected ENS namespace. - * - * @param namespaceId - * @param pluginName - * @returns registrar managed name - * @throws an error when no registrar managed name could be returned - */ -export function getRegistrarManagedName(namespaceId: ENSNamespaceId): RegistrarManagedName { - switch (namespaceId) { - case "mainnet": - return "linea.eth"; - case "sepolia": - return "linea-sepolia.eth"; - case "ens-test-env": - throw new Error( - `No registrar managed name is known for the Linea Names plugin within the "${namespaceId}" namespace.`, - ); - } -} diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/NameWrapper.ts index 3fb093715..8b667cf7e 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/NameWrapper.ts @@ -18,10 +18,7 @@ export default function () { handleNameWrapped, handleTransferBatch, handleTransferSingle, - } = makeNameWrapperHandlers({ - // the shared Registrar handlers in this plugin index direct subnames of '.eth' - registrarManagedName: "eth", - }); + } = makeNameWrapperHandlers(); ponder.on(namespaceContract(pluginName, "NameWrapper:NameWrapped"), handleNameWrapped); ponder.on(namespaceContract(pluginName, "NameWrapper:NameUnwrapped"), handleNameUnwrapped); 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 4681b314b..b324c5bcd 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts @@ -1,22 +1,11 @@ import { ponder } from "ponder:registry"; -import { type LabelHash, PluginName, uint256ToHex32 } from "@ensnode/ensnode-sdk"; +import { PluginName } from "@ensnode/ensnode-sdk"; +import { tokenIdToLabelHash } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { makeRegistrarHandlers } from "@/plugins/subgraph/shared-handlers/Registrar"; -/** - * When direct subnames of .eth are registered through the ETHRegistrarController contract on - * Ethereum mainnet, an ERC721 NFT is minted that tokenizes ownership of the registration. The minted NFT - * will be assigned a unique tokenId which is uint256(labelhash(label)) where label is the - * direct subname of .eth that was registered. - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 - */ -const tokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); - -// the shared Registrar handlers in this plugin index direct subnames of '.eth' -const registrarManagedName = "eth" as const; - /** * Registers event handlers with Ponder. */ @@ -29,10 +18,7 @@ export default function () { handleNameRenewedByController, handleNameRenewed, handleNameTransferred, - } = makeRegistrarHandlers({ - pluginName, - registrarManagedName, - }); + } = makeRegistrarHandlers({ pluginName }); /////////////////////////////// // BaseRegistrar diff --git a/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts index 5d1e22caf..3af42c319 100644 --- a/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts @@ -34,11 +34,12 @@ import { } from "@ensnode/ensnode-sdk"; import { subgraph_decodeDNSEncodedLiteralName } from "@/lib/dns-helpers"; +import { getThisAccountId } from "@/lib/get-this-account-id"; import { bigintMax } from "@/lib/lib-helpers"; +import { getManagedName } from "@/lib/managed-names"; import type { EventWithArgs } from "@/lib/ponder-helpers"; import { sharedEventValues, upsertAccount } from "@/lib/subgraph/db-helpers"; import { makeEventId } from "@/lib/subgraph/ids"; -import type { RegistrarManagedName } from "@/lib/types"; /** * When a name is wrapped in the NameWrapper contract, an ERC1155 token is minted that tokenizes @@ -121,16 +122,8 @@ async function materializeDomainExpiryDate(context: Context, node: Node) { /** * makes a set of shared handlers for the NameWrapper contract - * - * @param registrarManagedName the name of the Registrar that NameWrapper interacts with registers subnames of */ -export const makeNameWrapperHandlers = ({ - registrarManagedName, -}: { - registrarManagedName: RegistrarManagedName; -}) => { - const registrarManagedNode = namehash(registrarManagedName); - +export const makeNameWrapperHandlers = () => { async function handleTransfer( context: Context, event: EventWithArgs, @@ -250,6 +243,8 @@ export const makeNameWrapperHandlers = ({ }) { const { node, owner } = event.args; + const managedNode = namehash(getManagedName(getThisAccountId(context, event))); + await upsertAccount(context, owner); await context.db.update(schema.subgraph_domain, { id: node }).set((domain) => ({ @@ -259,7 +254,7 @@ export const makeNameWrapperHandlers = ({ // expiry to null because it does not expire. // via https://github.com/ensdomains/ens-subgraph/blob/c844791/src/nameWrapper.ts#L123 // NOTE: undefined = no change, null = null - expiryDate: domain.parentId === registrarManagedNode ? undefined : null, + expiryDate: domain.parentId === managedNode ? undefined : null, wrappedOwnerId: null, })); diff --git a/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts index be51fd305..054c7dfab 100644 --- a/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts @@ -18,13 +18,14 @@ import { type SubgraphInterpretedName, } from "@ensnode/ensnode-sdk"; +import { getThisAccountId } from "@/lib/get-this-account-id"; import { labelByLabelHash } from "@/lib/graphnode-helpers"; +import { getManagedName } from "@/lib/managed-names"; import { pluginSupportsPremintedNames } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; import { sharedEventValues, upsertAccount, upsertRegistration } from "@/lib/subgraph/db-helpers"; import { makeRegistrationId } from "@/lib/subgraph/ids"; import { isLabelSubgraphIndexable } from "@/lib/subgraph/is-label-subgraph-indexable"; -import type { RegistrarManagedName } from "@/lib/types"; import { handleNewOwner } from "@/plugins/subgraph/shared-handlers/Registry"; const GRACE_PERIOD_SECONDS = 7776000n; // 90 days in seconds @@ -35,21 +36,17 @@ const GRACE_PERIOD_SECONDS = 7776000n; // 90 days in seconds * @param pluginName the name of the plugin using these shared handlers * @param registrarManagedName the name that the Registrar contract indexes subnames of */ -export const makeRegistrarHandlers = ({ - pluginName, - registrarManagedName, -}: { - pluginName: PluginName; - registrarManagedName: RegistrarManagedName; -}) => { - const registrarManagedNode = namehash(registrarManagedName); - +export const makeRegistrarHandlers = ({ pluginName }: { pluginName: PluginName }) => { async function setNamePreimage( context: Context, - label: LiteralLabel, - labelHash: LabelHash, - cost: bigint, + event: EventWithArgs<{ + label: LiteralLabel; + labelHash: LabelHash; + cost: bigint; + }>, ) { + const { label, labelHash, cost } = event.args; + // NOTE(subgraph-compat): if the label is not subgraph-indexable, ignore it entirely if (config.isSubgraphCompatible && !isLabelSubgraphIndexable(label)) return; @@ -61,7 +58,8 @@ export const makeRegistrarHandlers = ({ // see https://ensnode.io/docs/reference/terminology#interpreted-label literalLabelToInterpretedLabel(label); - const node = makeSubdomainNode(labelHash, registrarManagedNode); + const managedNode = namehash(getManagedName(getThisAccountId(context, event))); + const node = makeSubdomainNode(labelHash, managedNode); const domain = await context.db.find(schema.subgraph_domain, { id: node }); // encode the runtime assertion here https://github.com/ensdomains/ens-subgraph/blob/c68a889/src/ethRegistrar.ts#L101 @@ -70,7 +68,7 @@ export const makeRegistrarHandlers = ({ // materialize the domain's name and labelName using the emitted values if (domain.labelName !== interpretedLabel) { // in either case a Name composed of (Subgraph) Interpreted Labels is (Subgraph) Interpreted - const interpretedName = `${interpretedLabel}.${registrarManagedName}` as + const interpretedName = `${interpretedLabel}.${managedNode}` as | InterpretedName | SubgraphInterpretedName; @@ -101,7 +99,9 @@ export const makeRegistrarHandlers = ({ await upsertAccount(context, owner); - const node = makeSubdomainNode(labelHash, registrarManagedNode); + const managedName = getManagedName(getThisAccountId(context, event)); + const managedNode = namehash(managedName); + const node = makeSubdomainNode(labelHash, managedNode); // NOTE(preminted-names): The mainnet ENS Registrar(s) _always_ register a node with the ENS // registry (emitting Registry#NewOwner) before emitting Registrar#NameRegistered. @@ -148,7 +148,7 @@ export const makeRegistrarHandlers = ({ ...event, args: { owner, - node: registrarManagedNode, + node: managedNode, label: labelHash, }, }, @@ -167,7 +167,7 @@ export const makeRegistrarHandlers = ({ // if subgraph-indexable, the label is Subgraph Interpreted label = healedLabel as Label as SubgraphInterpretedLabel; // a name constructed of Subgraph Interpreted Labels is Subgraph Interpreted - name = `${label}.${registrarManagedName}` as SubgraphInterpretedName; + name = `${label}.${managedName}` as SubgraphInterpretedName; } } else { // Interpret the `healedLabel` Literal Label into an Interpreted Label @@ -180,7 +180,7 @@ export const makeRegistrarHandlers = ({ ) as InterpretedLabel; // a name constructed of Interpreted Labels is Interpreted - name = `${label}.${registrarManagedName}` as InterpretedName; + name = `${label}.${managedName}` as InterpretedName; } // update Domain @@ -224,14 +224,10 @@ export const makeRegistrarHandlers = ({ cost: bigint; }>; }) { - const { label, labelHash, cost } = event.args; - - await setNamePreimage( - context, - label as LiteralLabel, // NameRegistered emits Literal Labels - labelHash, - cost, - ); + const { label: _label, labelHash, cost } = event.args; + const label = _label as LiteralLabel; // NameRegistered emits Literal Labels + + await setNamePreimage(context, { ...event, args: { label, labelHash, cost } }); }, async handleNameRenewedByController({ @@ -245,14 +241,10 @@ export const makeRegistrarHandlers = ({ cost: bigint; }>; }) { - const { label, labelHash, cost } = event.args; - - await setNamePreimage( - context, - label as LiteralLabel, // NameRenewed emits Literal Labels - labelHash, - cost, - ); + const { label: _label, labelHash, cost } = event.args; + const label = _label as LiteralLabel; // NameRenewed emits Literal Labels + + await setNamePreimage(context, { ...event, args: { label, labelHash, cost } }); }, async handleNameRenewed({ @@ -264,7 +256,8 @@ export const makeRegistrarHandlers = ({ }) { const { labelHash, expires } = event.args; - const node = makeSubdomainNode(labelHash, registrarManagedNode); + const managedNode = namehash(getManagedName(getThisAccountId(context, event))); + const node = makeSubdomainNode(labelHash, managedNode); const id = makeRegistrationId(labelHash, node); // update Registration expiry @@ -295,7 +288,8 @@ export const makeRegistrarHandlers = ({ // NOTE(subgraph-compat): despite the short-circuits below, upsertAccount must always be run await upsertAccount(context, to); - const node = makeSubdomainNode(labelHash, registrarManagedNode); + const managedNode = namehash(getManagedName(getThisAccountId(context, event))); + const node = makeSubdomainNode(labelHash, managedNode); const id = makeRegistrationId(labelHash, node); // if the Transfer event occurs before the Registration entity exists (i.e. the initial diff --git a/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx b/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx index 1b7d3512e..7a2834df0 100644 --- a/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx +++ b/docs/ensnode.io/src/content/docs/docs/concepts/what-is-ensnode.mdx @@ -68,7 +68,7 @@ Each plugin requires two files: Because plugins indexing subregistries use the shared handlers and may clobber entities created by the `subgraph` plugin—which didn't expect multichain or multi-source entities—, id-generating code is abstracted to be plugin-specific. See the helpers in `apps/ensindexer/src/lib/ids.ts`. In these cases, for the `subgraph` plugin, the original behavior is left un-modified to facilitate 1:1 responses from the subgraph-compatible api. -This scoping also applies to the concept of a `RegistrarManagedName` (see `apps/ensindexer/src/lib/types.ts` and `makeRegistrarHandlers` in `apps/ensindexer/src/handlers/Registrar.ts`) — teh shared handlers derived from the subgraph which are used by some plugins expect the context of a name whos subnames they manage. In the original subgraph implementation, this was hardcoded as the `.eth` name, and operations under the `Registrar` are in the context of direct subnames of `.eth`. +This scoping also applies to the concept of a "Managed Name" (see `apps/ensindexer/src/lib/managed-names.ts`): some contracts (like Registrars and RegistrarControllers) operate in the context of a Managed Name (like 'eth', 'base.eth', or 'linea.eth'). In the original subgraph implementation, this was hardcoded for the .eth Registrar/RegistrarControllers as the `eth` name, but because ENSNode shares indexing logic between these similar contracts across `.eth` names, `.base.eth` names, etc, we introduce the concept of a contract's "Managed Name" within the context of which it operates. ### Ponder Plugin Integration From c5a02cebe0b71627bdf2c686a17d9acb6427a2f7 Mon Sep 17 00:00:00 2001 From: shrugs Date: Wed, 24 Dec 2025 10:51:52 -0600 Subject: [PATCH 2/5] update docs --- apps/ensindexer/src/lib/managed-names.ts | 37 ++++++++++++++++-------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/apps/ensindexer/src/lib/managed-names.ts b/apps/ensindexer/src/lib/managed-names.ts index ace575f26..a55652b4a 100644 --- a/apps/ensindexer/src/lib/managed-names.ts +++ b/apps/ensindexer/src/lib/managed-names.ts @@ -15,21 +15,21 @@ import { import { toJson } from "@/lib/json-stringify-with-bigints"; /** - * Many Registrar contracts within the ENSv1 Ecosystem are relative to a parent Name. For example, + * Many contracts within the ENSv1 Ecosystem are relative to a parent Name. For example, * the .eth BaseRegistrar (and RegistrarConrollers) manage direct subnames of .eth. As such, they * operate on relative Labels, not fully qualified Names. We must know the parent name whose subnames * they manage in order to index them correctly. * * Because we use shared indexing logic for each instance of these contracts (BaseRegistrar, * RegistrarControllers, NameWrapper), the concept of "which name is this contract operating in the - * context of" must be generalizable: this is the Registrar Managed Name. + * context of" must be generalizable: this is the contract's 'Managed Name'. * - * Concretely, a .eth RegistrarController will emit a LabelHash indicating a new Registration, but + * Concretely, a .eth RegistrarController will emit a _LabelHash_ indicating a new Registration, but * correlating that LabelHash with the NameHash of the Name requires knowing the NameHash of the * Registrar's Managed Name ('eth' in this case). * * The NameWrapper contracts are relevant here as well because they include specialized logic for - * wrapping direct subnames of these Registrar Managed Names. + * wrapping direct subnames of these Managed Names. */ const ethnamesNameWrapper = getDatasourceContract( @@ -94,7 +94,11 @@ const CONTRACTS_BY_MANAGED_NAME: Record = { ), ].filter((c) => !!c), "linea.eth": [ - maybeGetDatasourceContract(config.namespace, DatasourceNames.Lineanames, "BaseRegistrar"), + maybeGetDatasourceContract( + config.namespace, // + DatasourceNames.Lineanames, + "BaseRegistrar", + ), maybeGetDatasourceContract( config.namespace, DatasourceNames.Lineanames, @@ -115,24 +119,24 @@ const MANAGED_NAME_BY_NAMESPACE: Partial { for (const [managedName, contracts] of Object.entries(CONTRACTS_BY_MANAGED_NAME)) { const isAnyOfTheContracts = contracts.some((_contract) => accountIdEqual(_contract, contract)); if (isAnyOfTheContracts) { - // override the default Managed Name with namespace-specific version if available + // use the namespace-specific Managed Name if specified, otherwise use the default from CONTRACTS_BY_MANAGED_NAME const namespaceSpecificManagedName = MANAGED_NAME_BY_NAMESPACE[config.namespace]?.[managedName] ?? managedName; return namespaceSpecificManagedName as InterpretedName; } } - throw new Error(`The following contract ${toJson(contract)} does not map to a Managed Name.`); + throw new Error(`The following contract ${toJson(contract)} does not have a Managed Name.`); }; /** - * Determines whether `contract` is the NameWrapper. + * Determines whether `contract` is a NameWrapper. */ export function isNameWrapper(contract: AccountId) { if (accountIdEqual(ethnamesNameWrapper, contract)) return true; @@ -141,9 +145,18 @@ export function isNameWrapper(contract: AccountId) { } /** - * BaseRegistrar-derived Registrars register direct subnames of a Managed Name. As such, the - * tokens issued by them are keyed by the direct subname's label's labelHash. This is identical for - * Basenames and Lineanames contracts as well. + * Decodes a uint256-encoded-LabelHash (i.e. a tokenId) into a {@link LabelHash}. + * + * Remember that contracts that operate in the context of a Managed Name frequently store and operate + * over _LabelHashes_ that represent a direct subname of a Managed Name. These contracts also frequently + * implement ERC721 or ERC1155 to represent ownership of these Names. As such, to construct the + * EC721/ERC1155 tokenId, they encode the direct subnames's LabelHash as a uint256. + * + * This is true for the ENSv1 BaseRegistrar, RegistrarControllers, and NameWrapper, as well as any + * contracts forked from from (which includes Basenames' and Lineanames' implementations). + * + * So, in order to turn the tokenId into a LabelHash, we perform the opposite operation, decoding + * from a uint256 into a Hex (of size 32) and cast it as our semantic {@link LabelHash} type. * * @see https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 * @see https://github.com/base/basenames/blob/1b5c1ad/src/L2/RegistrarController.sol#L488 From 6715e39e8ba732be0c139f8235ef211615035701 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 26 Dec 2025 15:21:03 -0600 Subject: [PATCH 3/5] feat: por review, docs, caching namehash --- apps/ensindexer/src/lib/managed-names.test.ts | 72 +++++++++++++++++++ apps/ensindexer/src/lib/managed-names.ts | 36 +++++++--- .../ensv2/handlers/ensv1/BaseRegistrar.ts | 8 +-- .../ensv2/handlers/ensv1/NameWrapper.ts | 6 +- .../handlers/ensv1/RegistrarController.ts | 6 +- .../basenames/handlers/Basenames_Registrar.ts | 7 +- .../handlers/Basenames_RegistrarController.ts | 11 ++- .../ethnames/handlers/Ethnames_Registrar.ts | 5 +- .../handlers/Ethnames_RegistrarController.ts | 13 ++-- ...s_UniversalRegistrarRenewalWithReferrer.ts | 3 +- .../handlers/Lineanames_Registrar.ts | 5 +- .../Lineanames_RegistrarController.ts | 9 ++- .../subgraph/shared-handlers/NameWrapper.ts | 4 +- .../subgraph/shared-handlers/Registrar.ts | 13 ++-- 14 files changed, 141 insertions(+), 57 deletions(-) create mode 100644 apps/ensindexer/src/lib/managed-names.test.ts diff --git a/apps/ensindexer/src/lib/managed-names.test.ts b/apps/ensindexer/src/lib/managed-names.test.ts new file mode 100644 index 000000000..14371003a --- /dev/null +++ b/apps/ensindexer/src/lib/managed-names.test.ts @@ -0,0 +1,72 @@ +import { namehash, zeroAddress } from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { type AccountId, ENSNamespaceIds, getDatasourceContract } from "@ensnode/ensnode-sdk"; + +import { getManagedName } from "./managed-names"; + +const { spy } = vi.hoisted(() => { + return { spy: vi.fn() }; +}); + +vi.mock("viem", async () => { + const actual = await vi.importActual("viem"); + return { + ...actual, + namehash: (name: string) => { + spy(name); + return actual.namehash(name); + }, + }; +}); + +// mock config.namespace as mainnet +vi.mock("@/config", () => ({ default: { namespace: ENSNamespaceIds.Mainnet } })); + +const registrar = getDatasourceContract( + ENSNamespaceIds.Mainnet, + DatasourceNames.ENSRoot, + "BaseRegistrar", +); + +const controller = getDatasourceContract( + ENSNamespaceIds.Mainnet, + DatasourceNames.ENSRoot, + "LegacyEthRegistrarController", +); + +const ETH_NODE = namehash("eth"); + +describe("managed-names", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + // NOTE: because the cache isn't resettable between test runs (exporting a reset method isn't worth), + // we simply enforce that the cache test case be run first via .sequential + describe.sequential("getManagedName", () => { + it("should cache the result of viem#namehash", () => { + expect(spy.mock.calls).toHaveLength(0); + + expect(getManagedName(registrar)).toStrictEqual({ name: "eth", node: ETH_NODE }); + + // first call should invoke namehash + expect(spy.mock.calls).toHaveLength(1); + + expect(getManagedName(controller)).toStrictEqual({ name: "eth", node: ETH_NODE }); + + // second call should not invoke namehash + expect(spy.mock.calls).toHaveLength(1); + }); + + it("should return the managed name and node for the BaseRegistrar contract", () => { + expect(getManagedName(registrar)).toStrictEqual({ name: "eth", node: ETH_NODE }); + }); + + it("should throw an error for a contract without a managed name", () => { + const unknownContract: AccountId = { chainId: 1, address: zeroAddress }; + expect(() => getManagedName(unknownContract)).toThrow("does not have a Managed Name"); + }); + }); +}); diff --git a/apps/ensindexer/src/lib/managed-names.ts b/apps/ensindexer/src/lib/managed-names.ts index a55652b4a..aca912e65 100644 --- a/apps/ensindexer/src/lib/managed-names.ts +++ b/apps/ensindexer/src/lib/managed-names.ts @@ -1,14 +1,16 @@ import config from "@/config"; +import { namehash } from "viem"; + import { DatasourceNames, type ENSNamespaceId } from "@ensnode/datasources"; import { type AccountId, accountIdEqual, getDatasourceContract, - type InterpretedName, type LabelHash, maybeGetDatasourceContract, type Name, + type Node, uint256ToHex32, } from "@ensnode/ensnode-sdk"; @@ -16,7 +18,7 @@ import { toJson } from "@/lib/json-stringify-with-bigints"; /** * Many contracts within the ENSv1 Ecosystem are relative to a parent Name. For example, - * the .eth BaseRegistrar (and RegistrarConrollers) manage direct subnames of .eth. As such, they + * the .eth BaseRegistrar (and RegistrarControllers) manage direct subnames of .eth. As such, they * operate on relative Labels, not fully qualified Names. We must know the parent name whose subnames * they manage in order to index them correctly. * @@ -29,7 +31,7 @@ import { toJson } from "@/lib/json-stringify-with-bigints"; * Registrar's Managed Name ('eth' in this case). * * The NameWrapper contracts are relevant here as well because they include specialized logic for - * wrapping direct subnames of these Managed Names. + * wrapping direct subnames of specific Managed Names. */ const ethnamesNameWrapper = getDatasourceContract( @@ -45,7 +47,8 @@ const lineanamesNameWrapper = maybeGetDatasourceContract( ); /** - * Mapping of a Managed Name to contracts that operate in the context of said Name. + * Mapping of a Managed Name to contracts that operate in the context of a (sub)Registry associated + * with that Name. */ const CONTRACTS_BY_MANAGED_NAME: Record = { eth: [ @@ -118,17 +121,32 @@ const MANAGED_NAME_BY_NAMESPACE: Partial(); +const cachedNamehash = (name: Name): Node => { + if (!namehashCache.has(name)) namehashCache.set(name, namehash(name)); + + // biome-ignore lint/style/noNonNullAssertion: guaranteed due to cache check above + return namehashCache.get(name)!; +}; + /** - * Given a `contract`, identify its Managed Name. + * Given a `contract`, identify its Managed Name and Node. + * + * @dev Caches the result of namehash(name). */ -export const getManagedName = (contract: AccountId) => { +export const getManagedName = (contract: AccountId): { name: Name; node: Node } => { for (const [managedName, contracts] of Object.entries(CONTRACTS_BY_MANAGED_NAME)) { const isAnyOfTheContracts = contracts.some((_contract) => accountIdEqual(_contract, contract)); if (isAnyOfTheContracts) { + const namespaceSpecific = MANAGED_NAME_BY_NAMESPACE[config.namespace]?.[managedName]; + // use the namespace-specific Managed Name if specified, otherwise use the default from CONTRACTS_BY_MANAGED_NAME - const namespaceSpecificManagedName = - MANAGED_NAME_BY_NAMESPACE[config.namespace]?.[managedName] ?? managedName; - return namespaceSpecificManagedName as InterpretedName; + const name = namespaceSpecific ?? managedName; + const node = cachedNamehash(name); + + return { name, node }; } } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index 3f3aca5f0..db627ab7c 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -1,7 +1,7 @@ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; import { GRACE_PERIOD_SECONDS } from "@ensdomains/ensjs/utils"; -import { type Address, isAddressEqual, namehash, zeroAddress } from "viem"; +import { type Address, isAddressEqual, zeroAddress } from "viem"; import { interpretAddress, @@ -72,7 +72,7 @@ export default function () { const labelHash = tokenIdToLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(registrar)); + const { node: managedNode } = getManagedName(registrar); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -103,7 +103,7 @@ export default function () { const labelHash = tokenIdToLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(registrar)); + const { node: managedNode } = getManagedName(registrar); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -161,7 +161,7 @@ export default function () { const labelHash = tokenIdToLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(registrar)); + const { node: managedNode } = getManagedName(registrar); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); const registration = await getLatestRegistration(context, domainId); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts index 5cf495574..874b15851 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -1,6 +1,6 @@ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { type Address, isAddressEqual, namehash, zeroAddress } from "viem"; +import { type Address, isAddressEqual, zeroAddress } from "viem"; import { type DNSEncodedLiteralName, @@ -187,12 +187,12 @@ export default function () { // handle wraps of direct-subname-of-registrar-managed-names if (registration && !isFullyExpired && registration.type === "BaseRegistrar") { - const managedNode = namehash(getManagedName(getThisAccountId(context, event))); + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); // Invariant: Emitted name is a direct subname of the Managed Name if (!isDirectSubnameOfManagedName(managedNode, name, node)) { throw new Error( - `Invariant(NameWrapper:NameWrapped): An unexpired BaseRegistrar Registration was found, but the name in question is NOT a direct subname of this NameWrapper's BaseRegistrar's Managed Name — wtf?`, + `Invariant(NameWrapper:NameWrapped): An unexpired BaseRegistrar Registration was found, but the name in question is NOT a direct subname of this NameWrapper's BaseRegistrar's Managed Name`, ); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts index 19b3dfbc0..300ab4204 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts @@ -1,7 +1,7 @@ /** biome-ignore-all lint/correctness/noUnusedVariables: ignore for now */ import { type Context, ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { labelhash, namehash } from "viem"; +import { labelhash } from "viem"; import { type EncodedReferrer, @@ -49,7 +49,7 @@ export default function () { } const controller = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(controller)); + const { node: managedNode } = getManagedName(controller); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -91,7 +91,7 @@ export default function () { const label = _label as LiteralLabel; const controller = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(controller)); + const { node: managedNode } = getManagedName(controller); const labelHash = labelhash(label); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); diff --git a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts index 724934c07..e8db2a92e 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts @@ -1,5 +1,4 @@ import { ponder } from "ponder:registry"; -import { namehash } from "viem/ens"; import { type BlockRef, @@ -32,7 +31,7 @@ export default function () { const id = event.id; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; const labelHash = tokenIdToLabelHash(event.args.id); @@ -65,7 +64,7 @@ export default function () { const id = event.id; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; const labelHash = tokenIdToLabelHash(event.args.id); @@ -98,7 +97,7 @@ export default function () { const id = event.id; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const labelHash = tokenIdToLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, managedNode); diff --git a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts index 619d8906f..ab54593d7 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts @@ -1,5 +1,4 @@ import { ponder } from "ponder:registry"; -import { namehash } from "viem/ens"; import { makeSubdomainNode, @@ -58,7 +57,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -89,7 +88,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -116,7 +115,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -147,7 +146,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -174,7 +173,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts index 5bfcd8d24..648022628 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts @@ -1,5 +1,4 @@ import { ponder } from "ponder:registry"; -import { namehash } from "viem/ens"; import { type BlockRef, @@ -31,7 +30,7 @@ export default function () { const id = event.id; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; const labelHash = tokenIdToLabelHash(event.args.id); @@ -64,7 +63,7 @@ export default function () { const id = event.id; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const labelHash = tokenIdToLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, managedNode); 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 75691a16e..0af1e320f 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts @@ -1,5 +1,4 @@ import { ponder } from "ponder:registry"; -import { namehash } from "viem"; import { addPrices, @@ -40,7 +39,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -89,7 +88,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -144,7 +143,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -192,7 +191,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -246,7 +245,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -297,7 +296,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts index 4a39ed565..18c7345af 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts @@ -1,5 +1,4 @@ import { ponder } from "ponder:registry"; -import { namehash } from "viem"; import { decodeEncodedReferrer, @@ -28,7 +27,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts index 1af7e1018..04c3b2a09 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts @@ -1,5 +1,4 @@ import { ponder } from "ponder:registry"; -import { namehash } from "viem/ens"; import { type BlockRef, @@ -31,7 +30,7 @@ export default function () { const id = event.id; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; const labelHash = tokenIdToLabelHash(event.args.id); @@ -64,7 +63,7 @@ export default function () { const id = event.id; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const labelHash = tokenIdToLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, managedNode); diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts index 413768ff6..19a181a1c 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts @@ -1,5 +1,4 @@ import { ponder } from "ponder:registry"; -import { namehash } from "viem/ens"; import { addPrices, @@ -47,7 +46,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -85,7 +84,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -123,7 +122,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; @@ -159,7 +158,7 @@ export default function () { } = event; const subregistryId = getThisAccountId(context, event); - const managedNode = namehash(getManagedName(subregistryId)); + const { node: managedNode } = getManagedName(subregistryId); const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; diff --git a/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts index 3af42c319..06580c5d0 100644 --- a/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts @@ -17,7 +17,7 @@ import schema from "ponder:schema"; * Related GitHub issue: https://github.com/ensdomains/ens-subgraph/issues/88 */ import { checkPccBurned as isPccFuseUnset } from "@ensdomains/ensjs/utils"; -import { type Address, namehash } from "viem"; +import type { Address } from "viem"; import { type DNSEncodedLiteralName, @@ -243,7 +243,7 @@ export const makeNameWrapperHandlers = () => { }) { const { node, owner } = event.args; - const managedNode = namehash(getManagedName(getThisAccountId(context, event))); + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); await upsertAccount(context, owner); diff --git a/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts index 054c7dfab..fa33d74e8 100644 --- a/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/shared-handlers/Registrar.ts @@ -2,7 +2,7 @@ import config from "@/config"; import type { Context } from "ponder:registry"; import schema from "ponder:schema"; -import { type Address, namehash } from "viem"; +import type { Address } from "viem"; import { encodeLabelHash, @@ -58,7 +58,7 @@ export const makeRegistrarHandlers = ({ pluginName }: { pluginName: PluginName } // see https://ensnode.io/docs/reference/terminology#interpreted-label literalLabelToInterpretedLabel(label); - const managedNode = namehash(getManagedName(getThisAccountId(context, event))); + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); const node = makeSubdomainNode(labelHash, managedNode); const domain = await context.db.find(schema.subgraph_domain, { id: node }); @@ -99,8 +99,9 @@ export const makeRegistrarHandlers = ({ pluginName }: { pluginName: PluginName } await upsertAccount(context, owner); - const managedName = getManagedName(getThisAccountId(context, event)); - const managedNode = namehash(managedName); + const { name: managedName, node: managedNode } = getManagedName( + getThisAccountId(context, event), + ); const node = makeSubdomainNode(labelHash, managedNode); // NOTE(preminted-names): The mainnet ENS Registrar(s) _always_ register a node with the ENS @@ -256,7 +257,7 @@ export const makeRegistrarHandlers = ({ pluginName }: { pluginName: PluginName } }) { const { labelHash, expires } = event.args; - const managedNode = namehash(getManagedName(getThisAccountId(context, event))); + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); const node = makeSubdomainNode(labelHash, managedNode); const id = makeRegistrationId(labelHash, node); @@ -288,7 +289,7 @@ export const makeRegistrarHandlers = ({ pluginName }: { pluginName: PluginName } // NOTE(subgraph-compat): despite the short-circuits below, upsertAccount must always be run await upsertAccount(context, to); - const managedNode = namehash(getManagedName(getThisAccountId(context, event))); + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); const node = makeSubdomainNode(labelHash, managedNode); const id = makeRegistrationId(labelHash, node); From 61656af00c74e650354337a73d8826fbd7ef097a Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 30 Dec 2025 13:30:59 -0600 Subject: [PATCH 4/5] feat: update naming and further refactor --- apps/ensindexer/src/lib/managed-names.ts | 34 ++++------------- .../src/lib/tokenscope/nft-issuers.ts | 36 ++++++------------ .../ensv2/handlers/ensv1/BaseRegistrar.ts | 9 +++-- .../ensv2/handlers/ensv1/NameWrapper.ts | 13 ++----- .../basenames/handlers/Basenames_Registrar.ts | 9 +++-- .../ethnames/handlers/Ethnames_Registrar.ts | 7 ++-- .../handlers/Lineanames_Registrar.ts | 7 ++-- .../plugins/basenames/handlers/Registrar.ts | 20 +++++++--- .../plugins/lineanames/handlers/Registrar.ts | 15 +++++--- .../plugins/subgraph/handlers/Registrar.ts | 9 ++--- .../subgraph/shared-handlers/NameWrapper.ts | 14 ++----- .../src/shared/interpretation/index.ts | 1 + .../interpretation/interpret-tokenid.ts | 38 +++++++++++++++++++ 13 files changed, 112 insertions(+), 100 deletions(-) create mode 100644 packages/ensnode-sdk/src/shared/interpretation/interpret-tokenid.ts diff --git a/apps/ensindexer/src/lib/managed-names.ts b/apps/ensindexer/src/lib/managed-names.ts index aca912e65..79bf5f7f4 100644 --- a/apps/ensindexer/src/lib/managed-names.ts +++ b/apps/ensindexer/src/lib/managed-names.ts @@ -7,11 +7,9 @@ import { type AccountId, accountIdEqual, getDatasourceContract, - type LabelHash, maybeGetDatasourceContract, type Name, type Node, - uint256ToHex32, } from "@ensnode/ensnode-sdk"; import { toJson } from "@/lib/json-stringify-with-bigints"; @@ -125,10 +123,12 @@ const MANAGED_NAME_BY_NAMESPACE: Partial(); const cachedNamehash = (name: Name): Node => { - if (!namehashCache.has(name)) namehashCache.set(name, namehash(name)); + const cached = namehashCache.get(name); + if (cached !== undefined) return cached; - // biome-ignore lint/style/noNonNullAssertion: guaranteed due to cache check above - return namehashCache.get(name)!; + const node = namehash(name); + namehashCache.set(name, node); + return node; }; /** @@ -150,7 +150,9 @@ export const getManagedName = (contract: AccountId): { name: Name; node: Node } } } - throw new Error(`The following contract ${toJson(contract)} does not have a Managed Name.`); + throw new Error( + `The following contract ${toJson(contract)} does not have a configured Managed Name. See apps/ensindexer/src/lib/managed-names.ts.`, + ); }; /** @@ -161,23 +163,3 @@ export function isNameWrapper(contract: AccountId) { if (lineanamesNameWrapper && accountIdEqual(lineanamesNameWrapper, contract)) return true; return false; } - -/** - * Decodes a uint256-encoded-LabelHash (i.e. a tokenId) into a {@link LabelHash}. - * - * Remember that contracts that operate in the context of a Managed Name frequently store and operate - * over _LabelHashes_ that represent a direct subname of a Managed Name. These contracts also frequently - * implement ERC721 or ERC1155 to represent ownership of these Names. As such, to construct the - * EC721/ERC1155 tokenId, they encode the direct subnames's LabelHash as a uint256. - * - * This is true for the ENSv1 BaseRegistrar, RegistrarControllers, and NameWrapper, as well as any - * contracts forked from from (which includes Basenames' and Lineanames' implementations). - * - * So, in order to turn the tokenId into a LabelHash, we perform the opposite operation, decoding - * from a uint256 into a Hex (of size 32) and cast it as our semantic {@link LabelHash} type. - * - * @see https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 - * @see https://github.com/base/basenames/blob/1b5c1ad/src/L2/RegistrarController.sol#L488 - * @see https://github.com/Consensys/linea-ens/blob/3a4f02f/packages/linea-ens-contracts/contracts/ethregistrar/ETHRegistrarController.sol#L447 - */ -export const tokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); diff --git a/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts b/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts index 0ac43078c..0be58ea9b 100644 --- a/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts +++ b/apps/ensindexer/src/lib/tokenscope/nft-issuers.ts @@ -8,13 +8,13 @@ import { type DomainAssetId, ETH_NODE, getDatasourceContract, - type LabelHash, + interpretTokenIdAsLabelHash, + interpretTokenIdAsNode, LINEANAMES_NODE, makeSubdomainNode, maybeGetDatasourceContract, type Node, type TokenId, - uint256ToHex32, } from "@ensnode/ensnode-sdk"; /** @@ -50,9 +50,7 @@ export interface SupportedNFTIssuer { * @param tokenId - The tokenId to convert * @returns The Node of the tokenId */ -const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => { - return uint256ToHex32(tokenId); -}; +const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => interpretTokenIdAsNode(tokenId); /** * Converts the tokenId from an ENS name token-issuing contract to a Node @@ -64,7 +62,7 @@ const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => { * @returns The Node of the tokenId issued under the parentNode */ const labelHashGeneratedTokenIdToNode = (tokenId: TokenId, parentNode: Node): Node => { - const labelHash: LabelHash = uint256ToHex32(tokenId); + const labelHash = interpretTokenIdAsLabelHash(tokenId); return makeSubdomainNode(labelHash, parentNode); }; @@ -112,9 +110,7 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC721, contract: ethBaseRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return labelHashGeneratedTokenIdToNode(tokenId, ETH_NODE); - }, + getDomainId: (tokenId: TokenId): Node => labelHashGeneratedTokenIdToNode(tokenId, ETH_NODE), }); } @@ -122,9 +118,7 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC1155, contract: nameWrapper, - getDomainId: (tokenId: TokenId): Node => { - return nameHashGeneratedTokenIdToNode(tokenId); - }, + getDomainId: nameHashGeneratedTokenIdToNode, }); } @@ -132,9 +126,7 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC1155, contract: threeDnsBaseRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return nameHashGeneratedTokenIdToNode(tokenId); - }, + getDomainId: nameHashGeneratedTokenIdToNode, }); } @@ -142,9 +134,7 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC1155, contract: threeDnsOptimismRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return nameHashGeneratedTokenIdToNode(tokenId); - }, + getDomainId: nameHashGeneratedTokenIdToNode, }); } @@ -152,9 +142,8 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC721, contract: lineanamesRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return labelHashGeneratedTokenIdToNode(tokenId, LINEANAMES_NODE); - }, + getDomainId: (tokenId: TokenId): Node => + labelHashGeneratedTokenIdToNode(tokenId, LINEANAMES_NODE), }); } @@ -162,9 +151,8 @@ const getSupportedNFTIssuers = (namespaceId: ENSNamespaceId): SupportedNFTIssuer result.push({ assetNamespace: AssetNamespaces.ERC721, contract: basenamesRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return labelHashGeneratedTokenIdToNode(tokenId, BASENAMES_NODE); - }, + getDomainId: (tokenId: TokenId): Node => + labelHashGeneratedTokenIdToNode(tokenId, BASENAMES_NODE), }); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts index db627ab7c..11e436340 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -5,6 +5,7 @@ import { type Address, isAddressEqual, zeroAddress } from "viem"; import { interpretAddress, + interpretTokenIdAsLabelHash, isRegistrationFullyExpired, makeENSv1DomainId, makeLatestRegistrationId, @@ -23,7 +24,7 @@ import { } from "@/lib/ensv2/registration-db-helpers"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { toJson } from "@/lib/json-stringify-with-bigints"; -import { getManagedName, tokenIdToLabelHash } from "@/lib/managed-names"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -70,7 +71,7 @@ export default function () { // // in all such cases, a Registration is expected and we can conditionally materialize Domain owner - const labelHash = tokenIdToLabelHash(tokenId); + const labelHash = interpretTokenIdAsLabelHash(tokenId); const registrar = getThisAccountId(context, event); const { node: managedNode } = getManagedName(registrar); const node = makeSubdomainNode(labelHash, managedNode); @@ -101,7 +102,7 @@ export default function () { const { id: tokenId, owner, expires: expiry } = event.args; const registrant = owner; - const labelHash = tokenIdToLabelHash(tokenId); + const labelHash = interpretTokenIdAsLabelHash(tokenId); const registrar = getThisAccountId(context, event); const { node: managedNode } = getManagedName(registrar); const node = makeSubdomainNode(labelHash, managedNode); @@ -159,7 +160,7 @@ export default function () { }) => { const { id: tokenId, expires: expiry } = event.args; - const labelHash = tokenIdToLabelHash(tokenId); + const labelHash = interpretTokenIdAsLabelHash(tokenId); const registrar = getThisAccountId(context, event); const { node: managedNode } = getManagedName(registrar); const node = makeSubdomainNode(labelHash, managedNode); diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts index 874b15851..cfb9e187d 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -7,6 +7,7 @@ import { type DNSEncodedName, decodeDNSEncodedLiteralName, interpretAddress, + interpretTokenIdAsNode, isPccFuseSet, isRegistrationExpired, isRegistrationFullyExpired, @@ -19,7 +20,6 @@ import { makeSubdomainNode, type Node, PluginName, - uint256ToHex32, } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; @@ -39,14 +39,6 @@ import type { EventWithArgs } from "@/lib/ponder-helpers"; const pluginName = PluginName.ENSv2; -/** - * When a name is wrapped in the NameWrapper contract, an ERC1155 token is minted that tokenizes - * ownership of the name. The minted token will be assigned a unique tokenId represented as - * uint256(namehash(name)) where name is the fqdn of the name being wrapped. - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/wrapper/ERC1155Fuse.sol#L262 - */ -const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); - /** * NameWrapper emits expiry as 0 to mean 'doesn't expire', so we interpret as null. */ @@ -120,7 +112,8 @@ export default function () { // otherwise is transfer of existing registration - const domainId = makeENSv1DomainId(tokenIdToNode(tokenId)); + // the NameWrapper's ERC1155 TokenIds are the ENSv1Domain's Node so we `interpretTokenIdAsNode` + const domainId = makeENSv1DomainId(interpretTokenIdAsNode(tokenId)); const registration = await getLatestRegistration(context, domainId); const isExpired = registration && isRegistrationExpired(registration, event.block.timestamp); diff --git a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts index e8db2a92e..9009c646e 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts @@ -3,13 +3,14 @@ import { ponder } from "ponder:registry"; import { type BlockRef, bigIntToNumber, + interpretTokenIdAsLabelHash, makeSubdomainNode, PluginName, type Subregistry, } from "@ensnode/ensnode-sdk"; import { getThisAccountId } from "@/lib/get-this-account-id"; -import { getManagedName, tokenIdToLabelHash } from "@/lib/managed-names"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { @@ -34,7 +35,7 @@ export default function () { const { node: managedNode } = getManagedName(subregistryId); const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; - const labelHash = tokenIdToLabelHash(event.args.id); + const labelHash = interpretTokenIdAsLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); @@ -67,7 +68,7 @@ export default function () { const { node: managedNode } = getManagedName(subregistryId); const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; - const labelHash = tokenIdToLabelHash(event.args.id); + const labelHash = interpretTokenIdAsLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); @@ -99,7 +100,7 @@ export default function () { const subregistryId = getThisAccountId(context, event); const { node: managedNode } = getManagedName(subregistryId); - const labelHash = tokenIdToLabelHash(event.args.id); + const labelHash = interpretTokenIdAsLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts index 648022628..ee93838d6 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts @@ -3,13 +3,14 @@ import { ponder } from "ponder:registry"; import { type BlockRef, bigIntToNumber, + interpretTokenIdAsLabelHash, makeSubdomainNode, PluginName, type Subregistry, } from "@ensnode/ensnode-sdk"; import { getThisAccountId } from "@/lib/get-this-account-id"; -import { getManagedName, tokenIdToLabelHash } from "@/lib/managed-names"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { @@ -33,7 +34,7 @@ export default function () { const { node: managedNode } = getManagedName(subregistryId); const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; - const labelHash = tokenIdToLabelHash(event.args.id); + const labelHash = interpretTokenIdAsLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); @@ -65,7 +66,7 @@ export default function () { const subregistryId = getThisAccountId(context, event); const { node: managedNode } = getManagedName(subregistryId); - const labelHash = tokenIdToLabelHash(event.args.id); + const labelHash = interpretTokenIdAsLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts index 04c3b2a09..723456344 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts @@ -3,13 +3,14 @@ import { ponder } from "ponder:registry"; import { type BlockRef, bigIntToNumber, + interpretTokenIdAsLabelHash, makeSubdomainNode, PluginName, type Subregistry, } from "@ensnode/ensnode-sdk"; import { getThisAccountId } from "@/lib/get-this-account-id"; -import { getManagedName, tokenIdToLabelHash } from "@/lib/managed-names"; +import { getManagedName } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { @@ -33,7 +34,7 @@ export default function () { const { node: managedNode } = getManagedName(subregistryId); const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; - const labelHash = tokenIdToLabelHash(event.args.id); + const labelHash = interpretTokenIdAsLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); @@ -65,7 +66,7 @@ export default function () { const subregistryId = getThisAccountId(context, event); const { node: managedNode } = getManagedName(subregistryId); - const labelHash = tokenIdToLabelHash(event.args.id); + const labelHash = interpretTokenIdAsLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, managedNode); const registrant = event.transaction.from; const expiresAt = bigIntToNumber(event.args.expires); diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts index 45e7f896d..c36b8958a 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/basenames/handlers/Registrar.ts @@ -1,8 +1,7 @@ import { ponder } from "ponder:registry"; -import { PluginName } from "@ensnode/ensnode-sdk"; +import { interpretTokenIdAsLabelHash, PluginName } from "@ensnode/ensnode-sdk"; -import { tokenIdToLabelHash } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { makeRegistrarHandlers } from "@/plugins/subgraph/shared-handlers/Registrar"; @@ -26,7 +25,10 @@ export default function () { async ({ context, event }) => { await handleNameRegistered({ context, - event: { ...event, args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.id) } }, + event: { + ...event, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.id) }, + }, }); }, ); @@ -36,7 +38,10 @@ export default function () { async ({ context, event }) => { await handleNameRegistered({ context, - event: { ...event, args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.id) } }, + event: { + ...event, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.id) }, + }, }); }, ); @@ -46,7 +51,10 @@ export default function () { async ({ context, event }) => { await handleNameRenewed({ context, - event: { ...event, args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.id) } }, + event: { + ...event, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.id) }, + }, }); }, ); @@ -56,7 +64,7 @@ export default function () { context, event: { ...event, - args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.tokenId) }, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.tokenId) }, }, }); }); diff --git a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts index b48405cd2..d37c68b03 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/lineanames/handlers/Registrar.ts @@ -1,8 +1,7 @@ import { ponder } from "ponder:registry"; -import { PluginName } from "@ensnode/ensnode-sdk"; +import { interpretTokenIdAsLabelHash, PluginName } from "@ensnode/ensnode-sdk"; -import { tokenIdToLabelHash } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { makeRegistrarHandlers } from "@/plugins/subgraph/shared-handlers/Registrar"; @@ -25,7 +24,10 @@ export default function () { async ({ context, event }) => { await handleNameRegistered({ context, - event: { ...event, args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.id) } }, + event: { + ...event, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.id) }, + }, }); }, ); @@ -35,7 +37,10 @@ export default function () { async ({ context, event }) => { await handleNameRenewed({ context, - event: { ...event, args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.id) } }, + event: { + ...event, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.id) }, + }, }); }, ); @@ -45,7 +50,7 @@ export default function () { context, event: { ...event, - args: { ...event.args, labelHash: tokenIdToLabelHash(event.args.tokenId) }, + args: { ...event.args, labelHash: interpretTokenIdAsLabelHash(event.args.tokenId) }, }, }); }); 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 b324c5bcd..293b0ad6b 100644 --- a/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts +++ b/apps/ensindexer/src/plugins/subgraph/plugins/subgraph/handlers/Registrar.ts @@ -1,8 +1,7 @@ import { ponder } from "ponder:registry"; -import { PluginName } from "@ensnode/ensnode-sdk"; +import { interpretTokenIdAsLabelHash, PluginName } from "@ensnode/ensnode-sdk"; -import { tokenIdToLabelHash } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import { makeRegistrarHandlers } from "@/plugins/subgraph/shared-handlers/Registrar"; @@ -34,7 +33,7 @@ export default function () { ...event, args: { ...event.args, - labelHash: tokenIdToLabelHash(event.args.id), + labelHash: interpretTokenIdAsLabelHash(event.args.id), }, }, }); @@ -50,7 +49,7 @@ export default function () { ...event, args: { ...event.args, - labelHash: tokenIdToLabelHash(event.args.id), + labelHash: interpretTokenIdAsLabelHash(event.args.id), }, }, }); @@ -66,7 +65,7 @@ export default function () { args: { from, to, - labelHash: tokenIdToLabelHash(tokenId), + labelHash: interpretTokenIdAsLabelHash(tokenId), }, }, }); diff --git a/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts b/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts index 06580c5d0..845995078 100644 --- a/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/subgraph/shared-handlers/NameWrapper.ts @@ -25,12 +25,12 @@ import { decodeDNSEncodedLiteralName, type InterpretedLabel, type InterpretedName, + interpretTokenIdAsNode, literalLabelsToInterpretedName, literalLabelToInterpretedLabel, type Node, type SubgraphInterpretedLabel, type SubgraphInterpretedName, - uint256ToHex32, } from "@ensnode/ensnode-sdk"; import { subgraph_decodeDNSEncodedLiteralName } from "@/lib/dns-helpers"; @@ -41,14 +41,6 @@ import type { EventWithArgs } from "@/lib/ponder-helpers"; import { sharedEventValues, upsertAccount } from "@/lib/subgraph/db-helpers"; import { makeEventId } from "@/lib/subgraph/ids"; -/** - * When a name is wrapped in the NameWrapper contract, an ERC1155 token is minted that tokenizes - * ownership of the name. The minted token will be assigned a unique tokenId represented as - * uint256(namehash(name)) where name is the fully qualified ENS name being wrapped. - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/wrapper/ERC1155Fuse.sol#L262 - */ -const tokenIdToNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); - /** * Determines whether the PCC fuse is SET in the provided `fuses`. */ @@ -132,7 +124,9 @@ export const makeNameWrapperHandlers = () => { to: Address, ) { await upsertAccount(context, to); - const node = tokenIdToNode(tokenId); + + // the NameWrapper's ERC1155 TokenIds are the ENSv1Domain's Node so we `interpretTokenIdAsNode` + const node = interpretTokenIdAsNode(tokenId); // NOTE: subgraph technically upserts domain with `createOrLoadDomain()` here, but domain // is guaranteed to exist. we encode this stricter logic here to illustrate that fact. diff --git a/packages/ensnode-sdk/src/shared/interpretation/index.ts b/packages/ensnode-sdk/src/shared/interpretation/index.ts index 1741768c1..1dbbd7d06 100644 --- a/packages/ensnode-sdk/src/shared/interpretation/index.ts +++ b/packages/ensnode-sdk/src/shared/interpretation/index.ts @@ -1,3 +1,4 @@ export * from "./interpret-address"; export * from "./interpret-record-values"; +export * from "./interpret-tokenid"; export * from "./interpreted-names-and-labels"; diff --git a/packages/ensnode-sdk/src/shared/interpretation/interpret-tokenid.ts b/packages/ensnode-sdk/src/shared/interpretation/interpret-tokenid.ts new file mode 100644 index 000000000..9903609d9 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/interpretation/interpret-tokenid.ts @@ -0,0 +1,38 @@ +import { type LabelHash, type Node, uint256ToHex32 } from "../../ens"; + +/** + * Decodes a uint256-encoded-LabelHash (eg. from a tokenId) into a {@link LabelHash}. + * + * Remember that contracts that operate in the context of a Managed Name frequently store and operate + * over _LabelHashes_ that represent a direct subname of a Managed Name. These contracts also frequently + * implement ERC721 or ERC1155 to represent ownership of these Names. As such, to construct the + * ERC721/ERC1155 tokenId, they may encode the direct subnames's LabelHash as a uint256. + * + * This is true for the ENSv1 BaseRegistrar, RegistrarControllers, as well as any + * contracts forked from it (which includes Basenames' and Lineanames' implementations). + * + * So, in order to turn the tokenId into a LabelHash, we perform the opposite operation, decoding + * from a uint256 into a Hex (of size 32) and cast it as our semantic {@link LabelHash} type. + * + * @see https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 + * @see https://github.com/base/basenames/blob/1b5c1ad/src/L2/RegistrarController.sol#L488 + * @see https://github.com/Consensys/linea-ens/blob/3a4f02f/packages/linea-ens-contracts/contracts/ethregistrar/ETHRegistrarController.sol#L447 + */ +export const interpretTokenIdAsLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); + +/** + * Decodes a uint256-encoded-Node (eg. from a tokenId) into a {@link Node}. + * + * Contracts in the ENSv1 ecosystem frequently implement ERC721 or ERC1155 to represent + * ownership of a Domain. As such, to construct the ERC721/ERC1155 tokenId, they may encode the + * domain's {@link Node} as a uint256. + * + * This is true for the ENSv1 NameWrapper, as well as any contracts forked from it (which includes + * Lineanames' implementation). + * + * So, in order to turn the tokenId into a Node, we perform the opposite operation, decoding + * from a uint256 into a Hex (of size 32) and cast it as our semantic {@link Node} type. + * + * @see https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/wrapper/ERC1155Fuse.sol#L262 + */ +export const interpretTokenIdAsNode = (tokenId: bigint): Node => uint256ToHex32(tokenId); From 35f39646d14525baaca64ca538a981e364251d3a Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 30 Dec 2025 15:06:19 -0600 Subject: [PATCH 5/5] fix test case --- apps/ensindexer/src/lib/managed-names.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensindexer/src/lib/managed-names.test.ts b/apps/ensindexer/src/lib/managed-names.test.ts index 14371003a..2f1b54949 100644 --- a/apps/ensindexer/src/lib/managed-names.test.ts +++ b/apps/ensindexer/src/lib/managed-names.test.ts @@ -66,7 +66,7 @@ describe("managed-names", () => { it("should throw an error for a contract without a managed name", () => { const unknownContract: AccountId = { chainId: 1, address: zeroAddress }; - expect(() => getManagedName(unknownContract)).toThrow("does not have a Managed Name"); + expect(() => getManagedName(unknownContract)).toThrow(); }); }); });