diff --git a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts b/apps/ensindexer/src/lib/ensv2/registrar-lib.ts deleted file mode 100644 index 62e78a075..000000000 --- a/apps/ensindexer/src/lib/ensv2/registrar-lib.ts +++ /dev/null @@ -1,130 +0,0 @@ -import config from "@/config"; - -import { DatasourceNames, type ENSNamespaceId } from "@ensnode/datasources"; -import { - type AccountId, - accountIdEqual, - getDatasourceContract, - type InterpretedName, - type LabelHash, - maybeGetDatasourceContract, - type Name, - uint256ToHex32, -} from "@ensnode/ensnode-sdk"; - -const ethnamesNameWrapper = getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "NameWrapper", -); - -const lineanamesNameWrapper = maybeGetDatasourceContract( - config.namespace, - DatasourceNames.Lineanames, - "NameWrapper", -); - -/** - * Mapping of RegistrarManagedName to its related Registrar and Registrar-adjacent contracts. - */ -const REGISTRAR_CONTRACTS_BY_MANAGED_NAME: Record = { - eth: [ - getDatasourceContract( - config.namespace, // - DatasourceNames.ENSRoot, - "BaseRegistrar", - ), - getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "LegacyEthRegistrarController", - ), - getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "WrappedEthRegistrarController", - ), - getDatasourceContract( - config.namespace, - DatasourceNames.ENSRoot, - "UnwrappedEthRegistrarController", - ), - ethnamesNameWrapper, - ], - "base.eth": [ - maybeGetDatasourceContract( - config.namespace, // - DatasourceNames.Basenames, - "BaseRegistrar", - ), - maybeGetDatasourceContract( - config.namespace, - DatasourceNames.Basenames, - "EARegistrarController", - ), - maybeGetDatasourceContract( - config.namespace, // - DatasourceNames.Basenames, - "RegistrarController", - ), - maybeGetDatasourceContract( - config.namespace, - DatasourceNames.Basenames, - "UpgradeableRegistrarController", - ), - ].filter((c) => !!c), - "linea.eth": [ - maybeGetDatasourceContract(config.namespace, DatasourceNames.Lineanames, "BaseRegistrar"), - maybeGetDatasourceContract( - config.namespace, - DatasourceNames.Lineanames, - "EthRegistrarController", - ), - lineanamesNameWrapper, - ].filter((c) => !!c), -}; - -/** - * Certain RegistrarManagedNames are different depending on the ENSNamespace — this encodes that - * relationship. - */ -const RMN_NAMESPACE_OVERRIDE: Partial>> = { - sepolia: { - "base.eth": "basetest.eth", - "linea.eth": "linea-sepolia.eth", - }, -}; - -/** - * Given a `contract`, identify its RegistrarManagedName. - */ -export const getRegistrarManagedName = (contract: AccountId) => { - for (const [managedName, contracts] of Object.entries(REGISTRAR_CONTRACTS_BY_MANAGED_NAME)) { - const isAnyOfTheContracts = contracts.some((_contract) => accountIdEqual(_contract, contract)); - if (isAnyOfTheContracts) { - const namespaceSpecificManagedName = - RMN_NAMESPACE_OVERRIDE[config.namespace]?.[managedName] ?? managedName; - // override the rmn with namespace-specific version if available - return namespaceSpecificManagedName as InterpretedName; - } - } - - throw new Error("never"); -}; - -/** - * Determines whether `contract` is the NameWrapper. - */ -export function isNameWrapper(contract: AccountId) { - if (accountIdEqual(ethnamesNameWrapper, contract)) return true; - if (lineanamesNameWrapper && accountIdEqual(lineanamesNameWrapper, contract)) return true; - return false; -} - -/** - * 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. - * - * https://github.com/ensdomains/ens-contracts/blob/db613bc/contracts/ethregistrar/ETHRegistrarController.sol#L215 - */ -export const registrarTokenIdToLabelHash = (tokenId: bigint): LabelHash => uint256ToHex32(tokenId); 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..2f1b54949 --- /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(); + }); + }); +}); diff --git a/apps/ensindexer/src/lib/managed-names.ts b/apps/ensindexer/src/lib/managed-names.ts new file mode 100644 index 000000000..79bf5f7f4 --- /dev/null +++ b/apps/ensindexer/src/lib/managed-names.ts @@ -0,0 +1,165 @@ +import config from "@/config"; + +import { namehash } from "viem"; + +import { DatasourceNames, type ENSNamespaceId } from "@ensnode/datasources"; +import { + type AccountId, + accountIdEqual, + getDatasourceContract, + maybeGetDatasourceContract, + type Name, + type Node, +} from "@ensnode/ensnode-sdk"; + +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 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. + * + * 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 contract's '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 specific Managed Names. + */ + +const ethnamesNameWrapper = getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "NameWrapper", +); + +const lineanamesNameWrapper = maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Lineanames, + "NameWrapper", +); + +/** + * 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: [ + getDatasourceContract( + config.namespace, // + DatasourceNames.ENSRoot, + "BaseRegistrar", + ), + getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "LegacyEthRegistrarController", + ), + getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "WrappedEthRegistrarController", + ), + getDatasourceContract( + config.namespace, + DatasourceNames.ENSRoot, + "UnwrappedEthRegistrarController", + ), + ethnamesNameWrapper, + ], + "base.eth": [ + maybeGetDatasourceContract( + config.namespace, // + DatasourceNames.Basenames, + "BaseRegistrar", + ), + maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Basenames, + "EARegistrarController", + ), + maybeGetDatasourceContract( + config.namespace, // + DatasourceNames.Basenames, + "RegistrarController", + ), + maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Basenames, + "UpgradeableRegistrarController", + ), + ].filter((c) => !!c), + "linea.eth": [ + maybeGetDatasourceContract( + config.namespace, // + DatasourceNames.Lineanames, + "BaseRegistrar", + ), + maybeGetDatasourceContract( + config.namespace, + DatasourceNames.Lineanames, + "EthRegistrarController", + ), + lineanamesNameWrapper, + ].filter((c) => !!c), +}; + +/** + * Certain Managed Names are different depending on the ENSNamespace — this encodes that relationship. + */ +const MANAGED_NAME_BY_NAMESPACE: Partial>> = { + sepolia: { + "base.eth": "basetest.eth", + "linea.eth": "linea-sepolia.eth", + }, +}; + +// Because we access a contract's Managed Name (and Node) frequently in event handlers, it's likely +// that caching the namehash() fn for these few values is beneficial, so we do so here. +const namehashCache = new Map(); +const cachedNamehash = (name: Name): Node => { + const cached = namehashCache.get(name); + if (cached !== undefined) return cached; + + const node = namehash(name); + namehashCache.set(name, node); + return node; +}; + +/** + * Given a `contract`, identify its Managed Name and Node. + * + * @dev Caches the result of namehash(name). + */ +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 name = namespaceSpecific ?? managedName; + const node = cachedNamehash(name); + + return { name, node }; + } + } + + throw new Error( + `The following contract ${toJson(contract)} does not have a configured Managed Name. See apps/ensindexer/src/lib/managed-names.ts.`, + ); +}; + +/** + * Determines whether `contract` is a NameWrapper. + */ +export function isNameWrapper(contract: AccountId) { + if (accountIdEqual(ethnamesNameWrapper, contract)) return true; + if (lineanamesNameWrapper && accountIdEqual(lineanamesNameWrapper, contract)) return true; + return false; +} 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/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..11e436340 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/BaseRegistrar.ts @@ -1,10 +1,11 @@ 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, + interpretTokenIdAsLabelHash, isRegistrationFullyExpired, makeENSv1DomainId, makeLatestRegistrationId, @@ -15,7 +16,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 +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 } from "@/lib/managed-names"; import { namespaceContract } from "@/lib/plugin-helpers"; import type { EventWithArgs } from "@/lib/ponder-helpers"; @@ -70,9 +71,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 = interpretTokenIdAsLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(registrar)); + const { node: managedNode } = getManagedName(registrar); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -101,9 +102,9 @@ export default function () { const { id: tokenId, owner, expires: expiry } = event.args; const registrant = owner; - const labelHash = registrarTokenIdToLabelHash(tokenId); + const labelHash = interpretTokenIdAsLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(registrar)); + const { node: managedNode } = getManagedName(registrar); const node = makeSubdomainNode(labelHash, managedNode); const domainId = makeENSv1DomainId(node); @@ -159,9 +160,9 @@ export default function () { }) => { const { id: tokenId, expires: expiry } = event.args; - const labelHash = registrarTokenIdToLabelHash(tokenId); + const labelHash = interpretTokenIdAsLabelHash(tokenId); const registrar = getThisAccountId(context, event); - const managedNode = namehash(getRegistrarManagedName(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 9e33eae2f..cfb9e187d 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/NameWrapper.ts @@ -1,12 +1,13 @@ 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, type DNSEncodedName, decodeDNSEncodedLiteralName, interpretAddress, + interpretTokenIdAsNode, isPccFuseSet, isRegistrationExpired, isRegistrationFullyExpired, @@ -19,13 +20,11 @@ import { makeSubdomainNode, type Node, PluginName, - uint256ToHex32, } from "@ensnode/ensnode-sdk"; 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,19 +33,12 @@ 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"; 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. */ @@ -64,7 +56,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 +70,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`, ); } @@ -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); @@ -187,12 +180,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 { node: managedNode } = 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`, ); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts index 9c68c6b2e..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, @@ -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 { 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(getRegistrarManagedName(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 deebd7c44..9009c646e 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, + interpretTokenIdAsLabelHash, makeSubdomainNode, PluginName, type Subregistry, } 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 { @@ -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 labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + 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 labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + 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 labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + 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..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,34 +1,23 @@ -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 +48,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -82,9 +79,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -101,9 +106,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -124,9 +137,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; await handleRegistrarControllerEvent(context, { @@ -143,9 +164,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 { node: managedNode } = 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..ee93838d6 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, + interpretTokenIdAsLabelHash, makeSubdomainNode, PluginName, type Subregistry, } 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 { @@ -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 labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + 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 labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + 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..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,13 +1,8 @@ -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 +11,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 +30,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); + const transactionHash = event.transaction.hash; /** * Ethnames_LegacyEthRegistrarController does not implement premiums, @@ -67,8 +65,6 @@ export default function () { decodedReferrer: null, } satisfies RegistrarActionReferralNotApplicable; - const transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, { id, subregistryId, @@ -83,9 +79,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); + const transactionHash = event.transaction.hash; /** * Ethnames_LegacyEthRegistrarController does not implement premiums, @@ -111,8 +116,6 @@ export default function () { decodedReferrer: null, } satisfies RegistrarActionReferralNotApplicable; - const transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, { id, subregistryId, @@ -131,9 +134,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -171,9 +182,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -217,9 +236,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -260,9 +287,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 { 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 d266b3856..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,41 +1,34 @@ -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 { node: managedNode } = 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..723456344 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, + interpretTokenIdAsLabelHash, makeSubdomainNode, PluginName, type Subregistry, } 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 { @@ -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 labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + const subregistry = { subregistryId, node: managedNode } satisfies Subregistry; + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + 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 labelHash = tokenIdToLabelHash(event.args.id); - const node = makeSubdomainNode(labelHash, parentNode); + + const subregistryId = getThisAccountId(context, event); + const { node: managedNode } = getManagedName(subregistryId); + + const labelHash = interpretTokenIdAsLabelHash(event.args.id); + 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..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,12 +1,7 @@ -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 +9,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 +20,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 +37,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -78,9 +75,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; /** @@ -108,9 +113,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 { node: managedNode } = getManagedName(subregistryId); + const node = makeSubdomainNode(labelHash, managedNode); const transactionHash = event.transaction.hash; const baseCost = priceEth(event.args.baseCost); @@ -136,9 +149,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 { node: managedNode } = 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..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,23 +1,10 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { type LabelHash, PluginName, uint256ToHex32 } from "@ensnode/ensnode-sdk"; +import { interpretTokenIdAsLabelHash, PluginName } from "@ensnode/ensnode-sdk"; 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 +17,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( @@ -43,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) }, + }, }); }, ); @@ -53,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) }, + }, }); }, ); @@ -63,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) }, + }, }); }, ); @@ -73,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/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..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,23 +1,10 @@ -import config from "@/config"; - import { ponder } from "ponder:registry"; -import { type LabelHash, PluginName, uint256ToHex32 } from "@ensnode/ensnode-sdk"; +import { interpretTokenIdAsLabelHash, PluginName } from "@ensnode/ensnode-sdk"; 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,19 +17,17 @@ 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"), 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) }, + }, }); }, ); @@ -52,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) }, + }, }); }, ); @@ -62,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/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..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,22 +1,10 @@ import { ponder } from "ponder:registry"; -import { type LabelHash, PluginName, uint256ToHex32 } from "@ensnode/ensnode-sdk"; +import { interpretTokenIdAsLabelHash, PluginName } from "@ensnode/ensnode-sdk"; 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 +17,7 @@ export default function () { handleNameRenewedByController, handleNameRenewed, handleNameTransferred, - } = makeRegistrarHandlers({ - pluginName, - registrarManagedName, - }); + } = makeRegistrarHandlers({ pluginName }); /////////////////////////////// // BaseRegistrar @@ -48,7 +33,7 @@ export default function () { ...event, args: { ...event.args, - labelHash: tokenIdToLabelHash(event.args.id), + labelHash: interpretTokenIdAsLabelHash(event.args.id), }, }, }); @@ -64,7 +49,7 @@ export default function () { ...event, args: { ...event.args, - labelHash: tokenIdToLabelHash(event.args.id), + labelHash: interpretTokenIdAsLabelHash(event.args.id), }, }, }); @@ -80,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 5d1e22caf..845995078 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, @@ -25,28 +25,21 @@ 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"; +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 - * 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`. @@ -121,16 +114,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, @@ -139,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. @@ -250,6 +237,8 @@ export const makeNameWrapperHandlers = ({ }) { const { node, owner } = event.args; + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); + await upsertAccount(context, owner); await context.db.update(schema.subgraph_domain, { id: node }).set((domain) => ({ @@ -259,7 +248,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..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, @@ -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 { node: managedNode } = 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,10 @@ export const makeRegistrarHandlers = ({ await upsertAccount(context, owner); - const node = makeSubdomainNode(labelHash, registrarManagedNode); + 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 // registry (emitting Registry#NewOwner) before emitting Registrar#NameRegistered. @@ -148,7 +149,7 @@ export const makeRegistrarHandlers = ({ ...event, args: { owner, - node: registrarManagedNode, + node: managedNode, label: labelHash, }, }, @@ -167,7 +168,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 +181,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 +225,10 @@ export const makeRegistrarHandlers = ({ cost: bigint; }>; }) { - const { label, labelHash, cost } = event.args; + const { label: _label, labelHash, cost } = event.args; + const label = _label as LiteralLabel; // NameRegistered emits Literal Labels - await setNamePreimage( - context, - label as LiteralLabel, // NameRegistered emits Literal Labels - labelHash, - cost, - ); + await setNamePreimage(context, { ...event, args: { label, labelHash, cost } }); }, async handleNameRenewedByController({ @@ -245,14 +242,10 @@ export const makeRegistrarHandlers = ({ cost: bigint; }>; }) { - const { label, labelHash, cost } = event.args; + const { label: _label, labelHash, cost } = event.args; + const label = _label as LiteralLabel; // NameRenewed emits Literal Labels - await setNamePreimage( - context, - label as LiteralLabel, // NameRenewed emits Literal Labels - labelHash, - cost, - ); + await setNamePreimage(context, { ...event, args: { label, labelHash, cost } }); }, async handleNameRenewed({ @@ -264,7 +257,8 @@ export const makeRegistrarHandlers = ({ }) { const { labelHash, expires } = event.args; - const node = makeSubdomainNode(labelHash, registrarManagedNode); + const { node: managedNode } = getManagedName(getThisAccountId(context, event)); + const node = makeSubdomainNode(labelHash, managedNode); const id = makeRegistrationId(labelHash, node); // update Registration expiry @@ -295,7 +289,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 { node: managedNode } = 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 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);