Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/quiet-yaks-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@ensnode/ensnode-schema": minor
"@ensnode/ensnode-sdk": minor
"ensindexer": minor
---

Introduces a new `registrars` plugin for tracking all registrations and renewals for direct subnames of `eth`, `base.eth`, and `linea.eth`.
6 changes: 6 additions & 0 deletions apps/ensindexer/ponder/src/register-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PluginName } from "@ensnode/ensnode-sdk";

import attach_protocolAccelerationHandlers from "@/plugins/protocol-acceleration/event-handlers";
import attach_ReferralHandlers from "@/plugins/referrals/event-handlers";
import attach_RegistrarsHandlers from "@/plugins/registrars/event-handlers";
import attach_BasenamesHandlers from "@/plugins/subgraph/plugins/basenames/event-handlers";
import attach_LineanamesHandlers from "@/plugins/subgraph/plugins/lineanames/event-handlers";
import attach_SubgraphHandlers from "@/plugins/subgraph/plugins/subgraph/event-handlers";
Expand Down Expand Up @@ -45,6 +46,11 @@ if (config.plugins.includes(PluginName.Referrals)) {
attach_ReferralHandlers();
}

// Registrars Plugin
if (config.plugins.includes(PluginName.Registrars)) {
attach_RegistrarsHandlers();
}

// TokenScope Plugin
if (config.plugins.includes(PluginName.TokenScope)) {
attach_TokenscopeHandlers();
Expand Down
2 changes: 2 additions & 0 deletions apps/ensindexer/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { MergedTypes } from "@/lib/lib-helpers";
// Core-Schema-Indepdendent Plugins
import protocolAccelerationPlugin from "./protocol-acceleration/plugin";
import referralsPlugin from "./referrals/plugin";
import registrarsPlugin from "./registrars/plugin";
// Subgraph-Schema Core Plugins
import basenamesPlugin from "./subgraph/plugins/basenames/plugin";
import lineaNamesPlugin from "./subgraph/plugins/lineanames/plugin";
Expand All @@ -20,6 +21,7 @@ export const ALL_PLUGINS = [
tokenScopePlugin,
protocolAccelerationPlugin,
referralsPlugin,
registrarsPlugin,
] as const;

/**
Expand Down
9 changes: 9 additions & 0 deletions apps/ensindexer/src/plugins/registrars/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# `registrars` plugin for ENSIndexer

This plugin enables tracking all registrations and renewals that ever happened for subregistries managing the following:
- direct subnames of the Ethnames registrar managed name (ex: `eth` for all namespaces).
- direct subnames of the Basenames registrar managed name (ex: for mainnet `base.eth` but varies for other namespaces).
- direct subnames of the Lineanames registrar managed name (ex: for mainnet `linea.eth` but varies for other namespaces).

Additionally indexes:
- All ENS Referrals (for Registrar Controllers supporting ENS Referral Programs).
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import config from "@/config";

import { ponder } from "ponder:registry";
import { namehash } from "viem/ens";

import { DatasourceNames } from "@ensnode/datasources";
import {
type BlockRef,
bigIntToNumber,
makeSubdomainNode,
PluginName,
type Subregistry,
} from "@ensnode/ensnode-sdk";

import { getDatasourceContract } from "@/lib/datasource-helpers";
import { namespaceContract } from "@/lib/plugin-helpers";

import {
handleRegistrarEventRegistration,
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 = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I note how we are building an object like this in several files. Should there be a ... satisfies X on each of these?

Appreciate if you can please update all the relevant files. Thanks

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 registrant = event.transaction.from;
const expiresAt = bigIntToNumber(event.args.expires);
const block = {
number: bigIntToNumber(event.block.number),
timestamp: bigIntToNumber(event.block.timestamp),
} satisfies BlockRef;
const transactionHash = event.transaction.hash;

await upsertSubregistry(context, subregistry);

await handleRegistrarEventRegistration(context, {
id,
subregistryId,
node,
registrant,
expiresAt,
block,
transactionHash,
});
},
);

ponder.on(
namespaceContract(pluginName, "Basenames_BaseRegistrar:NameRegistered"),
async ({ context, event }) => {
const id = event.id;
const labelHash = tokenIdToLabelHash(event.args.id);
const node = makeSubdomainNode(labelHash, parentNode);
const registrant = event.transaction.from;
const expiresAt = bigIntToNumber(event.args.expires);
const block = {
number: bigIntToNumber(event.block.number),
timestamp: bigIntToNumber(event.block.timestamp),
} satisfies BlockRef;
const transactionHash = event.transaction.hash;

await upsertSubregistry(context, subregistry);

await handleRegistrarEventRegistration(context, {
id,
subregistryId,
node,
registrant,
expiresAt,
block,
transactionHash,
});
},
);

ponder.on(
namespaceContract(pluginName, "Basenames_BaseRegistrar:NameRenewed"),
async ({ context, event }) => {
const id = event.id;
const labelHash = tokenIdToLabelHash(event.args.id);
const node = makeSubdomainNode(labelHash, parentNode);
const registrant = event.transaction.from;
const expiresAt = bigIntToNumber(event.args.expires);
const block = {
number: bigIntToNumber(event.block.number),
timestamp: bigIntToNumber(event.block.timestamp),
} satisfies BlockRef;
const transactionHash = event.transaction.hash;

await handleRegistrarEventRenewal(context, {
id,
subregistryId,
node,
registrant,
expiresAt,
block,
transactionHash,
});
},
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import config from "@/config";

import { ponder } from "ponder:registry";
import { namehash } from "viem/ens";

import { DatasourceNames } from "@ensnode/datasources";
import {
makeSubdomainNode,
PluginName,
type RegistrarActionPricingUnknown,
type RegistrarActionReferralNotApplicable,
} from "@ensnode/ensnode-sdk";

import { getDatasourceContract } from "@/lib/datasource-helpers";
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
* emits distinct baseCost or premium (as opposed to just a simple price)
* in events.
*
* TODO: [Index the pricing data for "logical registrar actions" for Basenames.](https://github.com/namehash/ensnode/issues/1256)
*/
const pricing = {
baseCost: null,
premium: null,
total: null,
} satisfies RegistrarActionPricingUnknown;

/**
* No Registrar Controller for Basenames implements referrals or
* emits a referrer in events.
*/
const referral = {
encodedReferrer: null,
decodedReferrer: null,
} satisfies RegistrarActionReferralNotApplicable;

/**
* Basenames_EARegistrarController Event Handlers
*/

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 transactionHash = event.transaction.hash;

await handleRegistrarControllerEvent(context, {
id,
subregistryId,
node,
pricing,
referral,
transactionHash,
});
},
);

/**
* Basenames_RegistrarController Event Handlers
*/

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 transactionHash = event.transaction.hash;

await handleRegistrarControllerEvent(context, {
id,
subregistryId,
node,
pricing,
referral,
transactionHash,
});
},
);

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 transactionHash = event.transaction.hash;

await handleRegistrarControllerEvent(context, {
id,
subregistryId,
node,
pricing,
referral,
transactionHash,
});
},
);

/**
* Basenames_UpgradeableRegistrarController Event Handlers
*/

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 transactionHash = event.transaction.hash;

await handleRegistrarControllerEvent(context, {
id,
subregistryId,
node,
pricing,
referral,
transactionHash,
});
},
);

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 transactionHash = event.transaction.hash;

await handleRegistrarControllerEvent(context, {
id,
subregistryId,
node,
pricing,
referral,
transactionHash,
});
},
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ENSNamespaceId } from "@ensnode/datasources";
import { 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 {
switch (namespaceId) {
case "mainnet":
return "base.eth";
case "sepolia":
return "basetest.eth";
case "holesky":
case "ens-test-env":
throw new Error(
`No registrar managed name is known for the 'basenames' subregistry within the "${namespaceId}" namespace.`,
);
}
}
Loading