From 4895125d8c197d3467430ead0158123bdac091b9 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 4 Nov 2025 21:03:33 +0100 Subject: [PATCH 01/13] feat(registrars): introduce db schema --- packages/ensnode-schema/src/ponder.schema.ts | 1 + .../src/schemas/registrars.schema.ts | 358 ++++++++++++++++++ 2 files changed, 359 insertions(+) create mode 100644 packages/ensnode-schema/src/schemas/registrars.schema.ts diff --git a/packages/ensnode-schema/src/ponder.schema.ts b/packages/ensnode-schema/src/ponder.schema.ts index 11ec675c79..bbe563a375 100644 --- a/packages/ensnode-schema/src/ponder.schema.ts +++ b/packages/ensnode-schema/src/ponder.schema.ts @@ -4,5 +4,6 @@ export * from "./schemas/protocol-acceleration.schema"; export * from "./schemas/referrals.schema"; +export * from "./schemas/registrars.schema"; export * from "./schemas/subgraph.schema"; export * from "./schemas/tokenscope.schema"; diff --git a/packages/ensnode-schema/src/schemas/registrars.schema.ts b/packages/ensnode-schema/src/schemas/registrars.schema.ts new file mode 100644 index 0000000000..159bcfe3dc --- /dev/null +++ b/packages/ensnode-schema/src/schemas/registrars.schema.ts @@ -0,0 +1,358 @@ +/** + * Schema Definitions for tracking of ENS subregistries. + */ + +import { index, onchainEnum, onchainTable, relations, uniqueIndex } from "ponder"; + +/** + * Subregistry + */ +export const subregistry = onchainTable( + "subregistries", + (t) => ({ + /** + * Subregistry ID + * + * Guaranteed to follow the CAIP-10 standard. + * + * @see https://chainagnostic.org/CAIPs/caip-10 + */ + subregistryId: t.text().primaryKey(), + + /** + * The node of a name the subregistry manager. Example managed names: + * - `eth` + * - `base.eth` + * - `linea.eth` + * + * Guaranteed to be a hex string representation of 32-bytes. + */ + node: t.hex().notNull(), + }), + (t) => ({ + uniqueNode: uniqueIndex().on(t.node), + }), +); + +/** + * Registration Lifecycle + */ +export const registrationLifecycle = onchainTable( + "registration_lifecycles", + (t) => ({ + /** + * The node of the FQDN of the domain this is associated with, + * guaranteed to be a subname of the associated subregistry + * for which the registration was executed. + * + * Guaranteed to be a hex string representation of 32-bytes. + */ + node: t.hex().primaryKey(), + + /** + * Subregistry ID + * + * Guaranteed to follow the CAIP-10 standard. + * + * @see https://chainagnostic.org/CAIPs/caip-10 + */ + subregistryId: t.text().notNull(), + + /** + * Unix timestamp when Registration Lifecycle is scheduled to expire. + */ + expiresAt: t.bigint().notNull(), + }), + (t) => ({ + bySubregistry: index().on(t.subregistryId), + }), +); + +/** + * Logical Registrar Action Type Enum + * + * Types of Logical Registrar Actions. + */ +export const logicalRegistrarActionType = onchainEnum("logical_registrar_action_type", [ + "registration", + "renewal", +]); + +/** + * Logical Registrar Action + * + * Records information indexed from multiple EVM events that happened in + * a single transaction. + * + * Consider the following situation: + * 1) When someone makes a new registration, multiple contracts take part in + * the registration process. + * 2) In order to build a single Logical Registrar Action record, + * we may need information from one or more events. For example, + * the `NameRegistered` event from the BaseRegistrar contract includes + * information for fields like: + * - `node` + * - `incrementalDuration` + * - `registrant` + * We use this event to initiate the Logical Registrar Action record. + * + * Another event we may index (and in most cases we do) is + * `NameRegistered` from RegistrarController contract, which may include: + * - `baseCost` + * - `premium` + * - `total` + * - `encodedReferrer` + * We use this event to update the Logical Registrar Action record. + * + * Both of those events contribute to a single Logical Registrar Action record. + */ +export const logicalRegistrarAction = onchainTable( + "logical_registrar_action", + (t) => ({ + /** + * Logical Registrar Action ID + * + * The `id` value is a deterministic identifier for the initial onchain event + * associated with the "logical" RegistrarAction, but the state recorded for + * a "logical" RegistrarAction may be an aggregate across multiple onchain + * events that may be distributed across multiple contracts (such as + * a RegistrarController and its associated BaseRegistrar). + */ + id: t.text().primaryKey(), + + /** + * Subregistry ID + * + * The ID of the subregistry which executed the Logical Registrar Action. + * + * Guaranteed to follow the CAIP-10 standard. + * + * @see https://chainagnostic.org/CAIPs/caip-10 + */ + subregistryId: t.text().notNull(), + + /** + * The node (namehash) of the name associated with the Logical Registrar + * Action. + * + * Guaranteed to be a hex string representation of 32-bytes. + */ + node: t.hex().notNull(), + + /** + * Type of the Logical Registrar Action. + */ + type: logicalRegistrarActionType().notNull(), + + /** + * Incremental Duration + * + * Definition of "incremental duration" is + * the incremental increase in the lifespan of the registration for + * `node` that was active as of `blockTimestamp`. + * + * Please consider the following situation: + * + * A registration of direct subname of .eth name is scheduled to expire on + * Jan 1, midnight UTC. It is currently 30 days after this expiration time. + * Therefore, there are currently another 60 days of grace period remaining + * for this name. Anyone can now make a renewal of this name. + * + * There are two possible scenarios when a renewal is made: + * + * 1) If a renewal is made for 10 days incremental duration, + * this name remains in an "expired" state, but it now + * has another 70 days of grace period remaining. + * + * 2) If a renewal is made for 50 days incremental duration, + * this name is no longer "expired" and is active, but it now + * expires in 20 days. + * + * After the latest registration of a direct subname becomes expired by + * more than the grace period, it can no longer be renewed by anyone. + * It must first be registered again, starting a new registration lifecycle of + * expiry / grace period / etc. + * + * Guaranteed to be a non-negative bigint value. + */ + incrementalDuration: t.bigint().notNull(), + + /** + * Base cost of the Logical Registrar Action. + * + * Guaranteed to be: + * 1) null if and only if `total` is null. + * 2) Otherwise, a non-negative bigint value for registrations. + */ + baseCost: t.bigint(), + + /** + * Premium of the Logical Registrar Action. + * + * Guaranteed to be: + * 1) null if and only if `total` is null. + * 2) Otherwise, zero when `type` is `renewal`. + * 3) Otherwise, a non-negative bigint value `type` is `registration`. + */ + premium: t.bigint(), + + /** + * Total cost of performing Logical Registrar Action. + * + * Guaranteed to be: + * 1) null if and only if both `baseCost` and `premium` are null. + * 2) Otherwise, a non-negative bigint value, equal to the sum of + * `baseCost` and `premium`. + */ + total: t.bigint(), + + /** + * Account that initiated the Logical Registrar Action and + * is paying the `total` cost. + */ + registrant: t.hex().notNull(), + + /** + * Encoded Referrer + * + * Represents the "raw" 32-byte "referrer" value emitted onchain in + * association with the registrar action. + * + * If a registrar / registrar controller doesn't support the concept of + * referrers then this field is set to null. + * + * Guaranteed to be: + * 1) null if a registrar / registrar controller doesn't support + * the concept of referrers. + * 2) Otherwise, a hex string representation of 32-bytes. + */ + encodedReferrer: t.hex(), + + /** + * Decoded referrer + * + * Guaranteed to be: + * 1) null if `encodedReferrer` is null. + * 2) Otherwise, a valid EVM address (including zero address). + */ + decodedReferrer: t.hex(), + + /** + * Number of the block that includes the Logical Registrar Action. + * + * Guaranteed to be a non-negative bigint value. + */ + blockNumber: t.bigint().notNull(), + + /** + * Timestamp of the block that includes the Logical Registrar Action. + * + * Guaranteed to be a non-negative bigint value. + */ + blockTimestamp: t.bigint().notNull(), + + /** + * Transaction hash of the transaction on `chainId` chain associated with + * the Logical Registrar Action. + * + * Guaranteed to be a string representation of 32-bytes. + */ + transactionHash: t.hex().notNull(), + + /** + * Event IDs + * + * An array of IDs referencing all onchain events, ordered by logIndex + * that have ever contributed to the state of the Logical Registrar Action. + * + * For example, the IDs will: + * 1) Always reference event emitted by BaseRegistrar contract. + * 2) Optionally reference event emitted by Registrar Controller contract, + * if and only if the given Registrar Controller contract is indexed. + * + * Note: Some Registrar Controller contracts that are not indexed + *. as they remain unknown to ENSIndexer at the moment. + * + * Logical Registrar Action ID value is guaranteed to be the initial + * element of that array. + */ + eventIds: t.text().array().notNull(), + }), + (t) => ({ + byRegistrant: index().on(t.registrant), + byDecodedReferrer: index().on(t.decodedReferrer), + byBlockTimestamp: index().on(t.blockTimestamp), + }), +); + +/** + * Logical Subregistry Action Metadata + * + * Building a single Logical Subregistry Action requires data from multiple + * onchain events. While handling the first event, we create a temporary + * Logical Subregistry Action Metadata record where we store `logicalEventId`. + * + * The `logicalEventId` is used by subsequent event handlers to update + * the Logical Subregistry Action record. In order to get `logicalEventId`, + * an event handler creates `logicalEventKey` from the currently handled + * onchain event. + * + * The very last event handler must remove the record referenced with + * `logicalEventKey` value. + */ +export const tempLogicalSubregistryAction = onchainTable( + "logical_subregistry_action_metadata", + (t) => ({ + /** + * Logical Event Key + * + * A string formatted as: (chainId, subregistryAddress, node, transactionHash). + */ + logicalEventKey: t.text().primaryKey(), + + /** + * Logical Event ID + * + * A string holding the ID value to an existing Logical Registrar Action + * record that was inserted while e use this event to initiate the Logical Registrar Action record. + */ + logicalEventId: t.text().notNull(), + }), +); + +/// Relations + +/** + * Subregistry Relations + * + * - many RegistrationLifecycles + */ +export const subregistryRelations = relations(subregistry, ({ many }) => ({ + registrationLifecycle: many(registrationLifecycle), +})); + +/** + * Registration Lifecycle Relations + * + * - exactly one Subregistry + */ +export const registrationLifecycleRelations = relations(registrationLifecycle, ({ one, many }) => ({ + subregistry: one(subregistry, { + fields: [registrationLifecycle.subregistryId], + references: [subregistry.subregistryId], + }), + + logicalRegistrarAction: many(logicalRegistrarAction), +})); + +/** + * Logical Registrar Action Relations + * + * - exactly one Registration Lifecycle + */ +export const logicalRegistrarActionRelations = relations(logicalRegistrarAction, ({ one }) => ({ + registrationLifecycle: one(registrationLifecycle, { + fields: [logicalRegistrarAction.node], + references: [registrationLifecycle.node], + }), +})); From 094d17b9aa64e5e389f92a369d7ea180a65b4222 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 5 Nov 2025 16:18:50 +0100 Subject: [PATCH 02/13] refactor(ensnode-schema): update registrars schema Drop "logical" from symbol names, keep the "logical" reference in JSDocs." --- .../src/schemas/registrars.schema.ts | 125 +++++++++--------- 1 file changed, 65 insertions(+), 60 deletions(-) diff --git a/packages/ensnode-schema/src/schemas/registrars.schema.ts b/packages/ensnode-schema/src/schemas/registrars.schema.ts index 159bcfe3dc..672a89cb97 100644 --- a/packages/ensnode-schema/src/schemas/registrars.schema.ts +++ b/packages/ensnode-schema/src/schemas/registrars.schema.ts @@ -13,14 +13,14 @@ export const subregistry = onchainTable( /** * Subregistry ID * - * Guaranteed to follow the CAIP-10 standard. + * Guaranteed to be a string formatted according to the CAIP-10 standard. * * @see https://chainagnostic.org/CAIPs/caip-10 */ subregistryId: t.text().primaryKey(), /** - * The node of a name the subregistry manager. Example managed names: + * The node of a name the subregistry manages. Example managed names: * - `eth` * - `base.eth` * - `linea.eth` @@ -52,7 +52,7 @@ export const registrationLifecycle = onchainTable( /** * Subregistry ID * - * Guaranteed to follow the CAIP-10 standard. + * Guaranteed to be a string formatted according to the CAIP-10 standard. * * @see https://chainagnostic.org/CAIPs/caip-10 */ @@ -69,32 +69,35 @@ export const registrationLifecycle = onchainTable( ); /** - * Logical Registrar Action Type Enum + * "Logical" Registrar Action Type Enum * - * Types of Logical Registrar Actions. + * Types of "logical" Registrar Actions. */ -export const logicalRegistrarActionType = onchainEnum("logical_registrar_action_type", [ +export const registrarActionType = onchainEnum("registrar_action_type", [ "registration", "renewal", ]); /** - * Logical Registrar Action + * "Logical" Registrar Action * - * Records information indexed from multiple EVM events that happened in + * Represents a "logical" RegistrarAction, but the state recorded for + * a "logical" RegistrarAction may be an aggregate across multiple onchain + * events that may be distributed across multiple contracts (such as + * a RegistrarController and its associated BaseRegistrar) within * a single transaction. * * Consider the following situation: * 1) When someone makes a new registration, multiple contracts take part in * the registration process. - * 2) In order to build a single Logical Registrar Action record, + * 2) In order to build a single "logical" Registrar Action record, * we may need information from one or more events. For example, * the `NameRegistered` event from the BaseRegistrar contract includes * information for fields like: * - `node` * - `incrementalDuration` * - `registrant` - * We use this event to initiate the Logical Registrar Action record. + * We use this event to initiate the "logical" Registrar Action record. * * Another event we may index (and in most cases we do) is * `NameRegistered` from RegistrarController contract, which may include: @@ -102,37 +105,36 @@ export const logicalRegistrarActionType = onchainEnum("logical_registrar_action_ * - `premium` * - `total` * - `encodedReferrer` - * We use this event to update the Logical Registrar Action record. + * We use this event to update the "logical" Registrar Action record. * - * Both of those events contribute to a single Logical Registrar Action record. + * Both of those events contribute to a single "logical" Registrar Action record. */ -export const logicalRegistrarAction = onchainTable( - "logical_registrar_action", +export const registrarAction = onchainTable( + "registrar_action", (t) => ({ /** - * Logical Registrar Action ID + * "Logical" Registrar Action ID * * The `id` value is a deterministic identifier for the initial onchain event - * associated with the "logical" RegistrarAction, but the state recorded for - * a "logical" RegistrarAction may be an aggregate across multiple onchain - * events that may be distributed across multiple contracts (such as - * a RegistrarController and its associated BaseRegistrar). + * associated with the "logical" RegistrarAction. + * + * Guaranteed to be the very first element in `eventIds` array. */ id: t.text().primaryKey(), /** * Subregistry ID * - * The ID of the subregistry which executed the Logical Registrar Action. + * The ID of the subregistry which executed the "logical" Registrar Action. * - * Guaranteed to follow the CAIP-10 standard. + * Guaranteed to be a string formatted according to the CAIP-10 standard. * * @see https://chainagnostic.org/CAIPs/caip-10 */ subregistryId: t.text().notNull(), /** - * The node (namehash) of the name associated with the Logical Registrar + * The node (namehash) of the name associated with the "logical" Registrar * Action. * * Guaranteed to be a hex string representation of 32-bytes. @@ -140,9 +142,9 @@ export const logicalRegistrarAction = onchainTable( node: t.hex().notNull(), /** - * Type of the Logical Registrar Action. + * Type of the "logical" Registrar Action. */ - type: logicalRegistrarActionType().notNull(), + type: registrarActionType().notNull(), /** * Incremental Duration @@ -178,7 +180,7 @@ export const logicalRegistrarAction = onchainTable( incrementalDuration: t.bigint().notNull(), /** - * Base cost of the Logical Registrar Action. + * Base cost of the "logical" Registrar Action. * * Guaranteed to be: * 1) null if and only if `total` is null. @@ -187,7 +189,7 @@ export const logicalRegistrarAction = onchainTable( baseCost: t.bigint(), /** - * Premium of the Logical Registrar Action. + * Premium of the "logical" Registrar Action. * * Guaranteed to be: * 1) null if and only if `total` is null. @@ -197,7 +199,7 @@ export const logicalRegistrarAction = onchainTable( premium: t.bigint(), /** - * Total cost of performing Logical Registrar Action. + * Total cost of performing the "logical" Registrar Action. * * Guaranteed to be: * 1) null if and only if both `baseCost` and `premium` are null. @@ -207,7 +209,7 @@ export const logicalRegistrarAction = onchainTable( total: t.bigint(), /** - * Account that initiated the Logical Registrar Action and + * Account that initiated the "logical" Registrar Action and * is paying the `total` cost. */ registrant: t.hex().notNull(), @@ -238,14 +240,14 @@ export const logicalRegistrarAction = onchainTable( decodedReferrer: t.hex(), /** - * Number of the block that includes the Logical Registrar Action. + * Number of the block that includes the "logical" Registrar Action. * * Guaranteed to be a non-negative bigint value. */ blockNumber: t.bigint().notNull(), /** - * Timestamp of the block that includes the Logical Registrar Action. + * Timestamp of the block that includes the "logical" Registrar Action. * * Guaranteed to be a non-negative bigint value. */ @@ -253,7 +255,7 @@ export const logicalRegistrarAction = onchainTable( /** * Transaction hash of the transaction on `chainId` chain associated with - * the Logical Registrar Action. + * the "logical" Registrar Action. * * Guaranteed to be a string representation of 32-bytes. */ @@ -263,7 +265,7 @@ export const logicalRegistrarAction = onchainTable( * Event IDs * * An array of IDs referencing all onchain events, ordered by logIndex - * that have ever contributed to the state of the Logical Registrar Action. + * that have ever contributed to the state of the "logical" Registrar Action. * * For example, the IDs will: * 1) Always reference event emitted by BaseRegistrar contract. @@ -273,8 +275,11 @@ export const logicalRegistrarAction = onchainTable( * Note: Some Registrar Controller contracts that are not indexed *. as they remain unknown to ENSIndexer at the moment. * - * Logical Registrar Action ID value is guaranteed to be the initial - * element of that array. + * The `id` value is guaranteed to be the initial element of that array. + * + * Guaranteed to: + * - Reference at least one event. + * - Keep event references ordered chronologically, by event log index. */ eventIds: t.text().array().notNull(), }), @@ -286,39 +291,38 @@ export const logicalRegistrarAction = onchainTable( ); /** - * Logical Subregistry Action Metadata + * "Logical" Subregistry Action Metadata * - * Building a single Logical Subregistry Action requires data from multiple + * Building a single "logical" Subregistry Action requires data from multiple * onchain events. While handling the first event, we create a temporary - * Logical Subregistry Action Metadata record where we store `logicalEventId`. + * "Logical" Subregistry Action Metadata record where we store `logicalEventId`. * * The `logicalEventId` is used by subsequent event handlers to update - * the Logical Subregistry Action record. In order to get `logicalEventId`, + * the "logical" Subregistry Action record. In order to get `logicalEventId`, * an event handler creates `logicalEventKey` from the currently handled * onchain event. * * The very last event handler must remove the record referenced with * `logicalEventKey` value. */ -export const tempLogicalSubregistryAction = onchainTable( - "logical_subregistry_action_metadata", - (t) => ({ - /** - * Logical Event Key - * - * A string formatted as: (chainId, subregistryAddress, node, transactionHash). - */ - logicalEventKey: t.text().primaryKey(), +export const tempLogicalSubregistryAction = onchainTable("_subregistry_action_metadata", (t) => ({ + /** + * Logical Event Key + * + * A string formatted as: + * `{chainId}:{subregistryAddress}:{node}:{transactionHash}` + */ + logicalEventKey: t.text().primaryKey(), - /** - * Logical Event ID - * - * A string holding the ID value to an existing Logical Registrar Action - * record that was inserted while e use this event to initiate the Logical Registrar Action record. - */ - logicalEventId: t.text().notNull(), - }), -); + /** + * Logical Event ID + * + * A string holding the ID value to an existing "logical" Registrar Action + * record that was inserted while e use this event to initiate + * the "logical" Registrar Action record. + */ + logicalEventId: t.text().notNull(), +})); /// Relations @@ -335,6 +339,7 @@ export const subregistryRelations = relations(subregistry, ({ many }) => ({ * Registration Lifecycle Relations * * - exactly one Subregistry + * - many "logical" RegistrarActions */ export const registrationLifecycleRelations = relations(registrationLifecycle, ({ one, many }) => ({ subregistry: one(subregistry, { @@ -342,17 +347,17 @@ export const registrationLifecycleRelations = relations(registrationLifecycle, ( references: [subregistry.subregistryId], }), - logicalRegistrarAction: many(logicalRegistrarAction), + registrarAction: many(registrarAction), })); /** - * Logical Registrar Action Relations + * "Logical" Registrar Action Relations * * - exactly one Registration Lifecycle */ -export const logicalRegistrarActionRelations = relations(logicalRegistrarAction, ({ one }) => ({ +export const logicalRegistrarActionRelations = relations(registrarAction, ({ one }) => ({ registrationLifecycle: one(registrationLifecycle, { - fields: [logicalRegistrarAction.node], + fields: [registrarAction.node], references: [registrationLifecycle.node], }), })); From 52b1a449f1722dff85655ab4694e7b5e847474c3 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 5 Nov 2025 21:36:04 +0100 Subject: [PATCH 03/13] feat(ensnode-sdk): improve shared modules Better type inference, support for bigint to number conversion. --- .../ensnode-sdk/src/shared/currencies.test.ts | 1 + packages/ensnode-sdk/src/shared/currencies.ts | 8 ++++--- .../ensnode-sdk/src/shared/deserialize.ts | 2 +- packages/ensnode-sdk/src/shared/index.ts | 1 + .../ensnode-sdk/src/shared/numbers.test.ts | 20 ++++++++++++++++++ packages/ensnode-sdk/src/shared/numbers.ts | 21 +++++++++++++++++++ 6 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 packages/ensnode-sdk/src/shared/numbers.test.ts create mode 100644 packages/ensnode-sdk/src/shared/numbers.ts diff --git a/packages/ensnode-sdk/src/shared/currencies.test.ts b/packages/ensnode-sdk/src/shared/currencies.test.ts index f06ed9729e..cdcee20022 100644 --- a/packages/ensnode-sdk/src/shared/currencies.test.ts +++ b/packages/ensnode-sdk/src/shared/currencies.test.ts @@ -108,6 +108,7 @@ describe("Currencies", () => { expect(addPrices(priceEth(1n), priceEth(2n), priceEth(3n))).toEqual(priceEth(6n)); }); it("throws an error if all prices do not have the same currency", () => { + // @ts-expect-error expect(() => addPrices(priceEth(1n), priceDai(2n), priceEth(3n))).toThrowError( /All prices must have the same currency to be added together/i, ); diff --git a/packages/ensnode-sdk/src/shared/currencies.ts b/packages/ensnode-sdk/src/shared/currencies.ts index 7b9c2f0680..5ddcc0a644 100644 --- a/packages/ensnode-sdk/src/shared/currencies.ts +++ b/packages/ensnode-sdk/src/shared/currencies.ts @@ -130,7 +130,9 @@ export function isPriceEqual(priceA: Price, priceB: Price): boolean { * @returns total of all prices. * @throws if not all prices have the same currency. */ -export function addPrices(...prices: [Price, Price, ...Price[]]): Price { +export function addPrices( + ...prices: [PriceType, PriceType, ...PriceType[]] +): PriceType { const firstPrice = prices[0]; const allPricesInSameCurrency = prices.every((price) => isPriceCurrencyEqual(firstPrice, price)); @@ -148,6 +150,6 @@ export function addPrices(...prices: [Price, Price, ...Price[]]): Price { { amount: 0n, currency: firstPrice.currency, - } satisfies Price, - ); + }, + ) as PriceType; } diff --git a/packages/ensnode-sdk/src/shared/deserialize.ts b/packages/ensnode-sdk/src/shared/deserialize.ts index f0b607416f..c4840d8041 100644 --- a/packages/ensnode-sdk/src/shared/deserialize.ts +++ b/packages/ensnode-sdk/src/shared/deserialize.ts @@ -102,7 +102,7 @@ export function deserializeBlockRef( return parsed.data; } -export function deserializeDuration(maybeDuration: string, valueLabel?: string): Duration { +export function deserializeDuration(maybeDuration: unknown, valueLabel?: string): Duration { const schema = makeDurationSchema(valueLabel); const parsed = schema.safeParse(maybeDuration); diff --git a/packages/ensnode-sdk/src/shared/index.ts b/packages/ensnode-sdk/src/shared/index.ts index 70eae532b9..c5712bc189 100644 --- a/packages/ensnode-sdk/src/shared/index.ts +++ b/packages/ensnode-sdk/src/shared/index.ts @@ -17,6 +17,7 @@ export { export * from "./interpretation"; export * from "./labelhash"; export * from "./null-bytes"; +export * from "./numbers"; export * from "./serialize"; export * from "./serialized-types"; export * from "./types"; diff --git a/packages/ensnode-sdk/src/shared/numbers.test.ts b/packages/ensnode-sdk/src/shared/numbers.test.ts new file mode 100644 index 0000000000..f6fce25a6b --- /dev/null +++ b/packages/ensnode-sdk/src/shared/numbers.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; + +import { bigIntToNumber } from "./numbers"; + +describe("Numbers", () => { + it("can convert bigint to number when possible", () => { + expect(bigIntToNumber(BigInt(Number.MAX_SAFE_INTEGER))).toEqual(Number.MAX_SAFE_INTEGER); + }); + + it("refuses to convert to low bigint value", () => { + expect(() => bigIntToNumber(BigInt(Number.MIN_SAFE_INTEGER - 1))).toThrowError( + /The bigint '-9007199254740992' value is too low to be to converted into a number/i, + ); + }); + it("refuses to convert to high bigint value", () => { + expect(() => bigIntToNumber(BigInt(Number.MAX_SAFE_INTEGER + 1))).toThrowError( + /The bigint '9007199254740992' value is too high to be to converted into a number/i, + ); + }); +}); diff --git a/packages/ensnode-sdk/src/shared/numbers.ts b/packages/ensnode-sdk/src/shared/numbers.ts new file mode 100644 index 0000000000..d788771d31 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/numbers.ts @@ -0,0 +1,21 @@ +/** + * Converts a bigint value into a number value. + * + * @throws when value is too low. + * @throws when value is too high . + */ +export function bigIntToNumber(n: bigint): number { + if (n < Number.MIN_SAFE_INTEGER) { + throw new Error( + `The bigint '${n.toString()}' value is too low to be to converted into a number.'`, + ); + } + + if (n > Number.MAX_SAFE_INTEGER) { + throw new Error( + `The bigint '${n.toString()}' value is too high to be to converted into a number.'`, + ); + } + + return Number(n); +} From 6d0b858c4bdd3ef5a8d41a8fd0b63578c5c07d21 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 5 Nov 2025 21:38:21 +0100 Subject: [PATCH 04/13] feat(ensnode-sdk): create `registrars` module The module includestypes and helpers supporting goals of the `registrars` plugin. --- packages/ensnode-sdk/package.json | 1 + packages/ensnode-sdk/src/index.ts | 1 + packages/ensnode-sdk/src/registrars/index.ts | 3 + .../src/registrars/registrar-action.ts | 268 ++++++++++++++++++ .../src/registrars/registration-lifecycle.ts | 66 +++++ .../ensnode-sdk/src/registrars/subregistry.ts | 20 ++ pnpm-lock.yaml | 3 + 7 files changed, 362 insertions(+) create mode 100644 packages/ensnode-sdk/src/registrars/index.ts create mode 100644 packages/ensnode-sdk/src/registrars/registrar-action.ts create mode 100644 packages/ensnode-sdk/src/registrars/registration-lifecycle.ts create mode 100644 packages/ensnode-sdk/src/registrars/subregistry.ts diff --git a/packages/ensnode-sdk/package.json b/packages/ensnode-sdk/package.json index c29462d3a6..490942d10b 100644 --- a/packages/ensnode-sdk/package.json +++ b/packages/ensnode-sdk/package.json @@ -55,6 +55,7 @@ "@adraffy/ens-normalize": "catalog:", "@ensdomains/address-encoder": "^1.1.2", "@ensnode/datasources": "workspace:*", + "@namehash/ens-referrals": "workspace:*", "caip": "catalog:", "zod": "catalog:" } diff --git a/packages/ensnode-sdk/src/index.ts b/packages/ensnode-sdk/src/index.ts index 48c6040203..aba4fcb957 100644 --- a/packages/ensnode-sdk/src/index.ts +++ b/packages/ensnode-sdk/src/index.ts @@ -6,6 +6,7 @@ export * from "./ensapi"; export * from "./ensindexer"; export * from "./ensrainbow"; export * from "./identity"; +export * from "./registrars"; export * from "./resolution"; export * from "./shared"; export * from "./tracing"; diff --git a/packages/ensnode-sdk/src/registrars/index.ts b/packages/ensnode-sdk/src/registrars/index.ts new file mode 100644 index 0000000000..d59bd32c9a --- /dev/null +++ b/packages/ensnode-sdk/src/registrars/index.ts @@ -0,0 +1,3 @@ +export * from "./registrar-action"; +export * from "./registration-lifecycle"; +export * from "./subregistry"; diff --git a/packages/ensnode-sdk/src/registrars/registrar-action.ts b/packages/ensnode-sdk/src/registrars/registrar-action.ts new file mode 100644 index 0000000000..a60cd3ca9a --- /dev/null +++ b/packages/ensnode-sdk/src/registrars/registrar-action.ts @@ -0,0 +1,268 @@ +import type { EncodedReferrer } from "@namehash/ens-referrals"; + +export type { EncodedReferrer } from "@namehash/ens-referrals"; +export { decodeEncodedReferrer, zeroEncodedReferrer } from "@namehash/ens-referrals"; + +import type { Address, Hash } from "viem"; + +import type { BlockRef, Duration, PriceEth } from "../shared"; +import type { RegistrationLifecycle } from "./registration-lifecycle"; + +/** + * Globally unique, deterministic ID of an indexed onchain event. + */ +type RegistrarActionEventId = string; + +/** + * Types of "logical" Registrar Action. + */ +export const RegistrarActionTypes = { + Registration: "registration", + Renewal: "renewal", +} as const; + +export type RegistrarActionType = (typeof RegistrarActionTypes)[keyof typeof RegistrarActionTypes]; + +/** + * Prices information for performing the "logical" registrar action. + */ +export interface RegistrarActionPricingAvailable { + /** + * Base cost + * + * Note: the "baseCost.amount" may be`0` or more. + */ + baseCost: PriceEth; + + /** + * Premium + * + * Note: the "premium.amount" may be`0` or more. + */ + premium: PriceEth; + + /** + * Total cost for performing the registrar action. + * + * Sum of `baseCost.amount` and `premium.amount`. + * + * Note: the "total.amount" may be`0` or more. + */ + total: PriceEth; +} + +/** + * Prices information for performing the "logical" registrar action. + */ +export interface RegistrarActionPricingNotApplicable { + /** + * Base cost + * + * Always null, as `total` is null. + */ + baseCost: null; + + /** + * Premium + * + * Always null, as `total` is null. + */ + premium: null; + + /** + * Total cost for performing the registrar action. + * + * Always null, as `baseCost` and `premium` are both null. + */ + total: null; +} + +export type RegistrarActionPricing = + | RegistrarActionPricingAvailable + | RegistrarActionPricingNotApplicable; + +export function isRegistrarActionPricingAvailable( + registrarActionPricing: RegistrarActionPricing, +): registrarActionPricing is RegistrarActionPricingAvailable { + const { baseCost, premium, total } = registrarActionPricing; + + return baseCost !== null && premium !== null && total !== null; +} + +/** + * Referrals information for performing the "logical" registrar action. + */ +export interface RegistrarActionReferralAvailable { + /** + * Encoded Referrer + * + * Represents the "raw" 32-byte "referrer" value emitted onchain in + * association with the registrar action. + * + * If a registrar / registrar controller supports the concept of + * referrers then this field is set (non-null). + */ + encodedReferrer: EncodedReferrer; + + /** + * Decoded Referrer + * + * Represents ENSNode's subjective interpretation of + * {@link RegistrarAction.encodedReferrer}. + * + * Invariants: + * - If the first `12`-bytes of "encodedReferrer" are all `0`, + * then "decodedReferrer" is the last `20`-bytes of "encodedReferrer", + * else: "decodedReferrer" is the zero address. + */ + decodedReferrer: Address; +} + +/** + * Referrals information for performing the "logical" registrar action. + */ +export interface RegistrarActionReferralNotApplicable { + /** + * Encoded Referrer + * + * Always null, as registrar / registrar controller doesn't support the concept of + * referrers. + */ + encodedReferrer: null; + + /** + * Decoded Referrer + * + * + * Always null, as `encodedReferrer` is null. + */ + decodedReferrer: null; +} + +export type RegistrarActionReferral = + | RegistrarActionReferralAvailable + | RegistrarActionReferralNotApplicable; + +export function isRegistrarActionReferralAvailable( + registrarActionReferral: RegistrarActionReferral, +): registrarActionReferral is RegistrarActionReferralAvailable { + const { encodedReferrer, decodedReferrer } = registrarActionReferral; + + return encodedReferrer !== null && decodedReferrer !== null; +} + +/** + * "Logical" Registrar Action + * + * Represents a state of "logical" Registrar Action. May be built using data + * from multiple events within the same "logical" registration / renewal action. + */ +export interface RegistrarAction { + /** + * Registrar Action ID + * + * This is ID of the event which initiated the "logical" Registrar Action. + */ + id: RegistrarActionEventId; + + /** + * Registrar Action Type + * + * The type of the Registrar Action. + */ + type: RegistrarActionType; + + /** + * Incremental Duration + * + * Represents the incremental increase in the duration of the lifespan of + * the registration for `node` that was active as of `timestamp`. + * Measured in seconds. + * + * A name with an active registration can be renewed at any time. + * + * Names that have expired may still be renewable. + * + * For example: assume the registration of a direct subname of Ethnames is + * scheduled to expire on Jan 1, midnight UTC. It is currently 30 days after + * this expiration time. Therefore, there are currently another 60 days of + * grace period remaining for this name. Anyone can still make + * a renewal of this name. + * + * Consider the following scenarios for renewals of a name that + * has expired but is still within its grace period: + * + * 1) Expired (in grace period) -> Expired (in grace period): + * If a renewal is made for 10 days incremental duration, + * this name remains in an "expired" (in grace period) state, but it now + * has 70 days of grace period remaining instead of only 60. + * + * 2) Expired (in grace period) -> Active: + * If a renewal is made for 50 days incremental duration, + * this name is no longer "expired" (in grace period) and is active, but it now + * expires and begins a new grace period in 20 days. + * + * After the latest registration of a direct subname becomes expired by + * more than the grace period, it can no longer be renewed by anyone. + * It must first be registered again, starting a new registration lifecycle of + * active / expiry / grace period / etc. + */ + incrementalDuration: Duration; + + /** + * Registrant + * + * Account that initiated the registrarAction and is paying the "total". + * It may not be the owner of the name: + * + * 1. When a name is registered, the initial owner of the name may be + * distinct from the registrant. + * 2. There are no restrictions on who may renew a name. + * Therefore the owner of the name may be distinct from the registrant. + */ + registrant: Address; + + /** + * Registration Lifecycle that this "logical" Registrar Action was + * executed for. + */ + registrationLifecycle: RegistrationLifecycle; + + /** + * Pricing information for performing this "logical" Registrar Action. + */ + pricing: RegistrarActionPricing; + + /** + * Referral information related to performing this "logical" Registrar Action. + */ + referral: RegistrarActionReferral; + + /** + * Block ref + * + * References the block where "logical" Registrar Action was executed. + */ + block: BlockRef; + + /** + * Transaction hash + * + * References the transaction within the `block` where + * the "logical" Registrar Action was executed. + */ + transactionHash: Hash; + + /** + * Event IDs + * + * An array of IDs referencing events which while being handled, + * contributed to the state of the "logical" Registrar Action. + * + * Guaranteed to: + * - Be ordered chronologically by event log index. + * - Have at least one element. + * - Reference the same value as `id` with its very first element. + */ + eventIds: [RegistrarActionEventId, ...RegistrarActionEventId[]]; +} diff --git a/packages/ensnode-sdk/src/registrars/registration-lifecycle.ts b/packages/ensnode-sdk/src/registrars/registration-lifecycle.ts new file mode 100644 index 0000000000..dc33939194 --- /dev/null +++ b/packages/ensnode-sdk/src/registrars/registration-lifecycle.ts @@ -0,0 +1,66 @@ +import type { Node } from "../ens"; +import type { UnixTimestamp } from "../shared"; +import type { Subregistry } from "./subregistry"; + +export const RegistrationLifecycleStages = { + /** + * Active + * + * Happens when + * the current timestamp <= expiry. + */ + Active: "registrationLifecycle_active", + + /** + * Grace Period + * + * Happens when + * `expiry < the current timestamp <= expiry + 90 days`. + */ + GracePeriod: "registrationLifecycle_gracePeriod", + + /** + * Released with Temporary Premium Price + * + * Happens when + * `expiry + 90 days < the current timestamp <= expiry + 120 days`. + */ + ReleasedWithTempPrice: "registrationLifecycle_releasedWithTempPrice", + + /** + * Fully Released (Regular Price) + * + * Happens when + * ` expiry + 120 days < the current timestamp`. + */ + FullyReleased: "registrationLifecycle_fullyReleased", +} as const; + +export type RegistrationLifecycleStage = + (typeof RegistrationLifecycleStages)[keyof typeof RegistrationLifecycleStages]; + +/** + * Registration Lifecycle + */ +export interface RegistrationLifecycle { + /** + * Subregistry account that this Registration Lifecycle belongs to. + */ + subregistry: Subregistry; + + /** + * The node of the FQDN of the domain this is associated with, + * guaranteed to be a subname of the associated subregistry + * for which the registration was executed. + */ + node: Node; + + /** + * Expires at + * + * The moment when the RegistrationLifecycle will transition + * from {@link RegistrationLifecycleStages.Active} + * to {@link RegistrationLifecycleStages.GracePeriod}. + */ + expiresAt: UnixTimestamp; +} diff --git a/packages/ensnode-sdk/src/registrars/subregistry.ts b/packages/ensnode-sdk/src/registrars/subregistry.ts new file mode 100644 index 0000000000..70016dfd92 --- /dev/null +++ b/packages/ensnode-sdk/src/registrars/subregistry.ts @@ -0,0 +1,20 @@ +import type { Node } from "../ens"; +import type { AccountId } from "../shared"; + +/** + * Subregistry + */ +export interface Subregistry { + /** + * Subregistry Account ID + */ + subregistryId: AccountId; + + /** + * The node of a name the subregistry manages. Example managed names: + * - `eth` + * - `base.eth` + * - `linea.eth` + */ + node: Node; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9297e7939..33319ca989 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -757,6 +757,9 @@ importers: '@ensnode/datasources': specifier: workspace:* version: link:../datasources + '@namehash/ens-referrals': + specifier: workspace:* + version: link:../ens-referrals caip: specifier: 'catalog:' version: 1.1.1 From f1fb889248e784b3243cb15b471ca1c1d3311b15 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 5 Nov 2025 21:39:50 +0100 Subject: [PATCH 05/13] feat(ensindexer): create `registrars` plugin --- .../ponder/src/register-handlers.ts | 6 + apps/ensindexer/src/plugins/index.ts | 2 + .../src/plugins/registrars/README.md | 10 + .../basenames/handlers/Basenames_Registrar.ts | 88 ++++ .../handlers/Basenames_RegistrarController.ts | 139 +++++++ .../basenames/lib/registrar-helpers.ts | 38 ++ .../ethnames/handlers/Ethnames_Registrar.ts | 68 ++++ .../handlers/Ethnames_RegistrarController.ts | 278 +++++++++++++ .../ethnames/lib/registrar-helpers.ts | 33 ++ .../src/plugins/registrars/event-handlers.ts | 17 + .../handlers/Lineanames_Registrar.ts | 68 ++++ .../Lineanames_RegistrarController.ts | 116 ++++++ .../lineanames/lib/registrar-helpers.ts | 38 ++ .../src/plugins/registrars/plugin.ts | 164 ++++++++ .../shared/lib/registrar-controller-events.ts | 117 ++++++ .../registrars/shared/lib/registrar-events.ts | 378 ++++++++++++++++++ .../registrars/shared/lib/subregistry.ts | 27 ++ .../src/schemas/registrars.schema.ts | 2 +- .../src/ensindexer/config/types.ts | 1 + 19 files changed, 1589 insertions(+), 1 deletion(-) create mode 100644 apps/ensindexer/src/plugins/registrars/README.md create mode 100644 apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts create mode 100644 apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts create mode 100644 apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts create mode 100644 apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts create mode 100644 apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts create mode 100644 apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts create mode 100644 apps/ensindexer/src/plugins/registrars/event-handlers.ts create mode 100644 apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts create mode 100644 apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts create mode 100644 apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts create mode 100644 apps/ensindexer/src/plugins/registrars/plugin.ts create mode 100644 apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts create mode 100644 apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts create mode 100644 apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts diff --git a/apps/ensindexer/ponder/src/register-handlers.ts b/apps/ensindexer/ponder/src/register-handlers.ts index dea35e4189..88f23449ef 100644 --- a/apps/ensindexer/ponder/src/register-handlers.ts +++ b/apps/ensindexer/ponder/src/register-handlers.ts @@ -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"; @@ -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(); diff --git a/apps/ensindexer/src/plugins/index.ts b/apps/ensindexer/src/plugins/index.ts index 66dd86cdbe..e948747c01 100644 --- a/apps/ensindexer/src/plugins/index.ts +++ b/apps/ensindexer/src/plugins/index.ts @@ -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"; @@ -20,6 +21,7 @@ export const ALL_PLUGINS = [ tokenScopePlugin, protocolAccelerationPlugin, referralsPlugin, + registrarsPlugin, ] as const; /** diff --git a/apps/ensindexer/src/plugins/registrars/README.md b/apps/ensindexer/src/plugins/registrars/README.md new file mode 100644 index 0000000000..4607fcbd3f --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/README.md @@ -0,0 +1,10 @@ +# `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 Registrar Controllers ever associated with a known Registrar contract. +- All ENS Referrals (for Registrar Controllers supporting ENS Referral Programs). diff --git a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts new file mode 100644 index 0000000000..3f05858176 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts @@ -0,0 +1,88 @@ +import config from "@/config"; + +import { ponder } from "ponder:registry"; +import { namehash } from "viem/ens"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; + +import { getDatasourceContract } from "@/lib/datasource-helpers"; +import { namespaceContract } from "@/lib/plugin-helpers"; + +import { handleRegistration, handleRenewal } 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, + }; + + // support NameRegisteredWithRecord for BaseRegistrar as it used by Base's RegistrarControllers + ponder.on( + namespaceContract(pluginName, "Basenames_BaseRegistrar:NameRegisteredWithRecord"), + async ({ context, event }) => { + const labelHash = tokenIdToLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, parentNode); + const expiresAt = bigIntToNumber(event.args.expires); + const registrant = event.transaction.from; + + await upsertSubregistry(context, subregistry); + + await handleRegistration(context, event, { + subregistryId, + node, + expiresAt, + registrant, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Basenames_BaseRegistrar:NameRegistered"), + async ({ context, event }) => { + const labelHash = tokenIdToLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, parentNode); + const expiresAt = bigIntToNumber(event.args.expires); + const registrant = event.transaction.from; + + await upsertSubregistry(context, subregistry); + + await handleRegistration(context, event, { + subregistryId, + node, + expiresAt, + registrant, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Basenames_BaseRegistrar:NameRenewed"), + async ({ context, event }) => { + const labelHash = tokenIdToLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, parentNode); + const expiresAt = bigIntToNumber(event.args.expires); + const registrant = event.transaction.from; + + await handleRenewal(context, event, { + subregistryId, + node, + expiresAt, + registrant, + }); + }, + ); +} diff --git a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts new file mode 100644 index 0000000000..fc0fc14575 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts @@ -0,0 +1,139 @@ +import config from "@/config"; + +import { ponder } from "ponder:registry"; +import { namehash } from "viem/ens"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { + makeSubdomainNode, + PluginName, + type RegistrarActionPricingNotApplicable, + 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. + */ + const pricing = { + baseCost: null, + premium: null, + total: null, + } satisfies RegistrarActionPricingNotApplicable; + + /** + * 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 labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + /** + * Basenames_RegistrarController Event Handlers + */ + + ponder.on( + namespaceContract(pluginName, "Basenames_RegistrarController:NameRegistered"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Basenames_RegistrarController:NameRenewed"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + /** + * Basenames_UpgradeableRegistrarController Event Handlers + */ + + ponder.on( + namespaceContract(pluginName, "Basenames_UpgradeableRegistrarController:NameRegistered"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Basenames_UpgradeableRegistrarController:NameRenewed"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); +} diff --git a/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts new file mode 100644 index 0000000000..2345b4ff7b --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/basenames/lib/registrar-helpers.ts @@ -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.`, + ); + } +} diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts new file mode 100644 index 0000000000..f831c6ae11 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts @@ -0,0 +1,68 @@ +import config from "@/config"; + +import { ponder } from "ponder:registry"; +import { namehash } from "viem/ens"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; + +import { getDatasourceContract } from "@/lib/datasource-helpers"; +import { namespaceContract } from "@/lib/plugin-helpers"; + +import { handleRegistration, handleRenewal } 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, + }; + + ponder.on( + namespaceContract(pluginName, "Ethnames_BaseRegistrar:NameRegistered"), + async ({ context, event }) => { + const labelHash = tokenIdToLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, parentNode); + const expiresAt = bigIntToNumber(event.args.expires); + const registrant = event.transaction.from; + + await upsertSubregistry(context, subregistry); + + await handleRegistration(context, event, { + subregistryId, + node, + expiresAt, + registrant, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Ethnames_BaseRegistrar:NameRenewed"), + async ({ context, event }) => { + const labelHash = tokenIdToLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, parentNode); + const expiresAt = bigIntToNumber(event.args.expires); + const registrant = event.transaction.from; + + await handleRenewal(context, event, { + subregistryId, + node, + expiresAt, + registrant, + }); + }, + ); +} diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts new file mode 100644 index 0000000000..1114c3101d --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts @@ -0,0 +1,278 @@ +import config from "@/config"; + +import { ponder } from "ponder:registry"; +import { namehash } from "viem"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { + addPrices, + decodeEncodedReferrer, + makeSubdomainNode, + PluginName, + priceEth, + type RegistrarActionPricingAvailable, + type RegistrarActionReferralAvailable, + 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.ENSRoot, + "BaseRegistrar", + ); + + /** + * Ethnames_LegacyEthRegistrarController Event Handlers + */ + + ponder.on( + namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController:NameRegistered"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + /** + * Ethnames_LegacyEthRegistrarController does not implement premiums, + * however, it implements base cost. + */ + const baseCost = priceEth(event.args.cost); + const premium = priceEth(0n); + const total = baseCost; + const pricing = { + baseCost, + premium, + total, + } satisfies RegistrarActionPricingAvailable; + + /** + * Ethnames_LegacyEthRegistrarController does not implement referrals or + * emits a referrer in events. + */ + const referral = { + encodedReferrer: null, + decodedReferrer: null, + } satisfies RegistrarActionReferralNotApplicable; + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController:NameRenewed"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + /** + * Ethnames_LegacyEthRegistrarController does not implement premiums, + * however, it implements base cost. + * + * Premium for renewals is always 0 anyway. + */ + const baseCost = priceEth(event.args.cost); + const premium = priceEth(0n); + const total = baseCost; + const pricing = { + baseCost, + premium, + total, + } satisfies RegistrarActionPricingAvailable; + + /** + * Ethnames_LegacyEthRegistrarController does not implement referrals or + * emits a referrer in events. + */ + const referral = { + encodedReferrer: null, + decodedReferrer: null, + } satisfies RegistrarActionReferralNotApplicable; + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + /** + * Ethnames_WrappedEthRegistrarController Event Handlers + */ + + ponder.on( + namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController:NameRegistered"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + /** + * Ethnames_WrappedEthRegistrarController implements premiums, and base cost. + */ + const baseCost = priceEth(event.args.baseCost); + const premium = priceEth(event.args.premium); + const total = addPrices(baseCost, premium); + const pricing = { + baseCost, + premium, + total, + } satisfies RegistrarActionPricingAvailable; + + /** + * Ethnames_WrappedEthRegistrarController does not implement referrals or + * emits a referrer in events. + */ + const referral = { + encodedReferrer: null, + decodedReferrer: null, + } satisfies RegistrarActionReferralNotApplicable; + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController:NameRenewed"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + /** + * Ethnames_WrappedEthRegistrarController implements premiums, and base cost. + * + * Premium for renewals is always 0 anyway. + */ + const baseCost = priceEth(event.args.cost); + const premium = priceEth(0n); + const total = baseCost; + const pricing = { + baseCost, + premium, + total, + } satisfies RegistrarActionPricingAvailable; + + /** + * Ethnames_WrappedEthRegistrarController does not implement referrals or + * emits a referrer in events. + */ + const referral = { + encodedReferrer: null, + decodedReferrer: null, + } satisfies RegistrarActionReferralNotApplicable; + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + /** + * Ethnames_UnwrappedEthRegistrarController Event Handlers + */ + + ponder.on( + namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController:NameRegistered"), + async ({ context, event }) => { + const labelHash = event.args.labelhash; + const node = makeSubdomainNode(labelHash, parentNode); + + /** + * Ethnames_UnwrappedEthRegistrarController implements premiums, and base cost. + */ + const baseCost = priceEth(event.args.baseCost); + const premium = priceEth(event.args.premium); + const total = addPrices(baseCost, premium); + const pricing = { + baseCost, + premium, + total, + } satisfies RegistrarActionPricingAvailable; + + /** + * Ethnames_UnwrappedEthRegistrarController implements referrals and + * emits a referrer in events. + */ + const encodedReferrer = event.args.referrer; + const decodedReferrer = decodeEncodedReferrer(encodedReferrer); + + const referral = { + encodedReferrer, + decodedReferrer, + } satisfies RegistrarActionReferralAvailable; + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController:NameRenewed"), + async ({ context, event }) => { + const labelHash = event.args.labelhash; + const node = makeSubdomainNode(labelHash, parentNode); + + /** + * Ethnames_UnwrappedEthRegistrarController implements premiums, and base cost. + * + * Premium for renewals is always 0 anyway. + */ + const baseCost = priceEth(event.args.cost); + const premium = priceEth(0n); + const total = baseCost; + const pricing = { + baseCost, + premium, + total, + } satisfies RegistrarActionPricingAvailable; + + /** + * Ethnames_UnwrappedEthRegistrarController implements referrals and + * emits a referrer in events. + */ + const encodedReferrer = event.args.referrer; + const decodedReferrer = decodeEncodedReferrer(encodedReferrer); + + const referral = { + encodedReferrer, + decodedReferrer, + } satisfies RegistrarActionReferralAvailable; + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); +} diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts new file mode 100644 index 0000000000..0ca10b56c1 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/ethnames/lib/registrar-helpers.ts @@ -0,0 +1,33 @@ +import type { ENSNamespaceId } from "@ensnode/datasources"; +import { 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 { + switch (namespaceId) { + case "mainnet": + case "sepolia": + case "holesky": + case "ens-test-env": + return "eth"; + } +} diff --git a/apps/ensindexer/src/plugins/registrars/event-handlers.ts b/apps/ensindexer/src/plugins/registrars/event-handlers.ts new file mode 100644 index 0000000000..d9b306224a --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/event-handlers.ts @@ -0,0 +1,17 @@ +import attach_Basenames_Registrars from "./basenames/handlers/Basenames_Registrar"; +import attach_Basenames_RegistrarControllers from "./basenames/handlers/Basenames_RegistrarController"; +import attach_Ethnames_Registrars from "./ethnames/handlers/Ethnames_Registrar"; +import attach_Ethnames_RegistrarControllers from "./ethnames/handlers/Ethnames_RegistrarController"; +import attach_Lineanames_Registrars from "./lineanames/handlers/Lineanames_Registrar"; +import attach_Lineanames_RegistrarControllers from "./lineanames/handlers/Lineanames_RegistrarController"; + +export default function () { + attach_Ethnames_Registrars(); + attach_Ethnames_RegistrarControllers(); + + attach_Basenames_Registrars(); + attach_Basenames_RegistrarControllers(); + + attach_Lineanames_Registrars(); + attach_Lineanames_RegistrarControllers(); +} diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts new file mode 100644 index 0000000000..8f1d8e2cd2 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts @@ -0,0 +1,68 @@ +import config from "@/config"; + +import { ponder } from "ponder:registry"; +import { namehash } from "viem/ens"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; + +import { getDatasourceContract } from "@/lib/datasource-helpers"; +import { namespaceContract } from "@/lib/plugin-helpers"; + +import { handleRegistration, handleRenewal } 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, + }; + + ponder.on( + namespaceContract(pluginName, "Lineanames_BaseRegistrar:NameRegistered"), + async ({ context, event }) => { + const labelHash = tokenIdToLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, parentNode); + const expiresAt = bigIntToNumber(event.args.expires); + const registrant = event.transaction.from; + + await upsertSubregistry(context, subregistry); + + await handleRegistration(context, event, { + subregistryId, + node, + expiresAt, + registrant, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Lineanames_BaseRegistrar:NameRenewed"), + async ({ context, event }) => { + const labelHash = tokenIdToLabelHash(event.args.id); + const node = makeSubdomainNode(labelHash, parentNode); + const expiresAt = bigIntToNumber(event.args.expires); + const registrant = event.transaction.from; + + await handleRenewal(context, event, { + subregistryId, + node, + expiresAt, + registrant, + }); + }, + ); +} diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts new file mode 100644 index 0000000000..2684243775 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts @@ -0,0 +1,116 @@ +import config from "@/config"; + +import { ponder } from "ponder:registry"; +import { namehash } from "viem/ens"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { + makeSubdomainNode, + PluginName, + type RegistrarActionPricingNotApplicable, + type RegistrarActionReferralNotApplicable, +} from "@ensnode/ensnode-sdk"; + +import { getDatasourceContract } from "@/lib/datasource-helpers"; +import { namespaceContract } from "@/lib/plugin-helpers"; + +import { getRegistrarManagedName } from "../../lineanames/lib/registrar-helpers"; +import { handleRegistrarControllerEvent } from "../../shared/lib/registrar-controller-events"; + +/** + * 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", + ); + + /** + * No Registrar Controller for Lineanames implements premiums or + * emits distinct baseCost or premium (as opposed to just a simple price) + * in events. + */ + const pricing = { + baseCost: null, + premium: null, + total: null, + } satisfies RegistrarActionPricingNotApplicable; + + /** + * No Registrar Controller for Lineanames implements referrals or + * emits a referrer in events. + */ + const referral = { + encodedReferrer: null, + decodedReferrer: null, + } satisfies RegistrarActionReferralNotApplicable; + + /** + * Lineanames_EthRegistrarController Event Handlers + */ + + ponder.on( + namespaceContract(pluginName, "Lineanames_EthRegistrarController:OwnerNameRegistered"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Lineanames_EthRegistrarController:PohNameRegistered"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Lineanames_EthRegistrarController:NameRegistered"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); + + ponder.on( + namespaceContract(pluginName, "Lineanames_EthRegistrarController:NameRenewed"), + async ({ context, event }) => { + const labelHash = event.args.label; // this field is the labelhash, not the label + const node = makeSubdomainNode(labelHash, parentNode); + + await handleRegistrarControllerEvent(context, event, { + subregistryId, + node, + pricing, + referral, + }); + }, + ); +} diff --git a/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts b/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts new file mode 100644 index 0000000000..13bba68f31 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/lineanames/lib/registrar-helpers.ts @@ -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 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 { + switch (namespaceId) { + case "mainnet": + return "linea.eth"; + case "sepolia": + return "linea-sepolia.eth"; + case "holesky": + case "ens-test-env": + throw new Error( + `No registrar managed name is known for the 'lineanames' subregistry within the "${namespaceId}" namespace.`, + ); + } +} diff --git a/apps/ensindexer/src/plugins/registrars/plugin.ts b/apps/ensindexer/src/plugins/registrars/plugin.ts new file mode 100644 index 0000000000..164b4fe440 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/plugin.ts @@ -0,0 +1,164 @@ +/** + * The `registrars` plugin indexes data about ENS subregistries, specifically the + * registrar and registrar controller contracts that manage registrations and renewals + * for known subregistry base registrars for the following: + * - Ethnames + * - Basenames + * - Lineanames + */ + +import * as ponder from "ponder"; + +import { DatasourceNames } from "@ensnode/datasources"; +import { PluginName } from "@ensnode/ensnode-sdk"; + +import { + createPlugin, + getDatasourceAsFullyDefinedAtCompileTime, + namespaceContract, +} from "@/lib/plugin-helpers"; +import { chainConfigForContract, chainsConnectionConfig } from "@/lib/ponder-helpers"; + +const pluginName = PluginName.Registrars; + +export default createPlugin({ + name: pluginName, + requiredDatasourceNames: [ + DatasourceNames.ENSRoot, + DatasourceNames.Basenames, + DatasourceNames.Lineanames, + ], + createPonderConfig(config) { + // configure Ethnames dependencies + const ethnamesDatasource = getDatasourceAsFullyDefinedAtCompileTime( + config.namespace, + DatasourceNames.ENSRoot, + ); + + const ethnamesRegistrarContracts = { + [namespaceContract(pluginName, "Ethnames_BaseRegistrar")]: { + chain: chainConfigForContract( + config.globalBlockrange, + ethnamesDatasource.chain.id, + ethnamesDatasource.contracts.BaseRegistrar, + ), + abi: ethnamesDatasource.contracts.BaseRegistrar.abi, + }, + }; + + const ethnamesRegistrarControllerContracts = { + [namespaceContract(pluginName, "Ethnames_LegacyEthRegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + ethnamesDatasource.chain.id, + ethnamesDatasource.contracts.LegacyEthRegistrarController, + ), + abi: ethnamesDatasource.contracts.LegacyEthRegistrarController.abi, + }, + [namespaceContract(pluginName, "Ethnames_WrappedEthRegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + ethnamesDatasource.chain.id, + ethnamesDatasource.contracts.WrappedEthRegistrarController, + ), + abi: ethnamesDatasource.contracts.WrappedEthRegistrarController.abi, + }, + [namespaceContract(pluginName, "Ethnames_UnwrappedEthRegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + ethnamesDatasource.chain.id, + ethnamesDatasource.contracts.UnwrappedEthRegistrarController, + ), + abi: ethnamesDatasource.contracts.UnwrappedEthRegistrarController.abi, + }, + }; + + // configure Basenames dependencies + const basenamesDatasource = getDatasourceAsFullyDefinedAtCompileTime( + config.namespace, + DatasourceNames.Basenames, + ); + + const basenamesRegistrarContracts = { + [namespaceContract(pluginName, "Basenames_BaseRegistrar")]: { + chain: chainConfigForContract( + config.globalBlockrange, + basenamesDatasource.chain.id, + basenamesDatasource.contracts.BaseRegistrar, + ), + abi: basenamesDatasource.contracts.BaseRegistrar.abi, + }, + }; + + const basenamesRegistrarControllerContracts = { + [namespaceContract(pluginName, "Basenames_EARegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + basenamesDatasource.chain.id, + basenamesDatasource.contracts.EARegistrarController, + ), + abi: basenamesDatasource.contracts.EARegistrarController.abi, + }, + [namespaceContract(pluginName, "Basenames_RegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + basenamesDatasource.chain.id, + basenamesDatasource.contracts.RegistrarController, + ), + abi: basenamesDatasource.contracts.RegistrarController.abi, + }, + [namespaceContract(pluginName, "Basenames_UpgradeableRegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + basenamesDatasource.chain.id, + basenamesDatasource.contracts.UpgradeableRegistrarController, + ), + abi: basenamesDatasource.contracts.UpgradeableRegistrarController.abi, + }, + }; + + // configure Lineanames dependencies + const linenamesDatasource = getDatasourceAsFullyDefinedAtCompileTime( + config.namespace, + DatasourceNames.Lineanames, + ); + + const lineanamesRegistrarContracts = { + [namespaceContract(pluginName, "Lineanames_BaseRegistrar")]: { + chain: chainConfigForContract( + config.globalBlockrange, + linenamesDatasource.chain.id, + linenamesDatasource.contracts.BaseRegistrar, + ), + abi: linenamesDatasource.contracts.BaseRegistrar.abi, + }, + }; + + const lineanamesRegistrarControllerContracts = { + [namespaceContract(pluginName, "Lineanames_EthRegistrarController")]: { + chain: chainConfigForContract( + config.globalBlockrange, + linenamesDatasource.chain.id, + linenamesDatasource.contracts.EthRegistrarController, + ), + abi: linenamesDatasource.contracts.EthRegistrarController.abi, + }, + }; + + return ponder.createConfig({ + chains: { + ...chainsConnectionConfig(config.rpcConfigs, ethnamesDatasource.chain.id), + ...chainsConnectionConfig(config.rpcConfigs, basenamesDatasource.chain.id), + ...chainsConnectionConfig(config.rpcConfigs, linenamesDatasource.chain.id), + }, + contracts: { + ...ethnamesRegistrarContracts, + ...ethnamesRegistrarControllerContracts, + ...basenamesRegistrarContracts, + ...basenamesRegistrarControllerContracts, + ...lineanamesRegistrarContracts, + ...lineanamesRegistrarControllerContracts, + }, + }); + }, +}); diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts new file mode 100644 index 0000000000..2ea1c75b16 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts @@ -0,0 +1,117 @@ +import type { Context, Event } from "ponder:registry"; +import schema from "ponder:schema"; +import type { Address } from "viem"; + +import { + type AccountId, + type EncodedReferrer, + isRegistrarActionPricingAvailable, + isRegistrarActionReferralAvailable, + type Node, + type RegistrarActionPricing, + type RegistrarActionReferral, +} from "@ensnode/ensnode-sdk"; + +import { type LogicalEventKey, makeLogicalEventKey } from "../../shared/lib/registrar-events"; + +/** + * Get "logical" Registrar Action record by logical event key. + * + * @throws if the record cannot be found. + */ +async function getLogicalRegistrarAction(context: Context, logicalEventKey: LogicalEventKey) { + const tempRecord = await context.db.find(schema.tempLogicalSubregistryAction, { + logicalEventKey, + }); + + // Invariant: the "logical" Registrar Action ID must be available + if (!tempRecord) { + throw new Error( + `Handling Registrar Controller Registration action requires the "logical" Registrar Action ID, which could not be found for the following logical event key: '${logicalEventKey}'.`, + ); + } + + const { logicalEventId } = tempRecord; + + const logicalRegistrarAction = await context.db.find(schema.registrarAction, { + id: logicalEventId, + }); + + // Invariant: the "logical" Registrar Action record must be available + if (!logicalRegistrarAction) { + throw new Error( + `Handling Registrar Controller Registration action requires the "logical" Registrar Action record, which could not be found for the following logical event ID: '${logicalEventId}'.`, + ); + } + + return logicalRegistrarAction; +} + +/** + * Update the "logical" Registrar Action: + * - set pricing data (if available) + * - set referral data (if available) + * - append new event ID to `eventIds` + */ +export async function handleRegistrarControllerEvent( + context: Context, + event: Event, + { + subregistryId, + node, + pricing, + referral, + }: { + subregistryId: AccountId; + node: Node; + pricing: RegistrarActionPricing; + referral: RegistrarActionReferral; + }, +) { + const logicalEventKey = makeLogicalEventKey({ + subregistryId, + node, + transactionHash: event.transaction.hash, + }); + + // get the "logical" Registrar Action to update + const { id } = await getLogicalRegistrarAction(context, logicalEventKey); + + // get pricing info + let baseCost: bigint | null; + let premium: bigint | null; + let total: bigint | null; + + if (isRegistrarActionPricingAvailable(pricing)) { + baseCost = pricing.baseCost.amount; + premium = pricing.premium.amount; + total = pricing.total.amount; + } else { + baseCost = null; + premium = null; + total = null; + } + + // get referral info + let encodedReferrer: EncodedReferrer | null; + let decodedReferrer: Address | null; + + if (isRegistrarActionReferralAvailable(referral)) { + encodedReferrer = referral.encodedReferrer; + decodedReferrer = referral.decodedReferrer; + } else { + encodedReferrer = null; + decodedReferrer = null; + } + + // update pricing data & referral data accordingly + // plus, append new event id to `eventIds` + await context.db.update(schema.registrarAction, { id }).set((logicalRegistrarAction) => ({ + baseCost, + premium, + total, + encodedReferrer, + decodedReferrer, + eventIds: [...logicalRegistrarAction.eventIds, event.id], + })); +} diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts new file mode 100644 index 0000000000..6fade308d2 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts @@ -0,0 +1,378 @@ +/** + * This file contains handlers used in event handlers for a Registrar contract. + */ + +import type { Context, Event } from "ponder:registry"; +import schema from "ponder:schema"; +import type { Address, Hash } from "viem"; + +import { + type AccountId, + bigIntToNumber, + deserializeDuration, + type Node, + type RegistrarAction, + RegistrarActionTypes, + serializeAccountId, + type UnixTimestamp, +} from "@ensnode/ensnode-sdk"; + +/** + * Logical Event Key + * + * String formatted as: + * `{accountId}:{node}:{transactionHash}`, where `accountId` follows + * the CAIP-10 standard. + * + * @see https://chainagnostic.org/CAIPs/caip-10 + */ +export type LogicalEventKey = string; + +export function makeLogicalEventKey({ + subregistryId, + node, + transactionHash, +}: { + subregistryId: AccountId; + node: Node; + transactionHash: Hash; +}): LogicalEventKey { + return [serializeAccountId(subregistryId), node, transactionHash].join(":"); +} + +async function getSubregistry(context: Context, { subregistryId }: { subregistryId: AccountId }) { + return context.db.find(schema.subregistry, { subregistryId: serializeAccountId(subregistryId) }); +} + +async function getRegistrationLifecycle(context: Context, { node }: { node: Node }) { + return context.db.find(schema.registrationLifecycle, { node }); +} + +/** + * Make first registration + * + * Inserts a new record to track the current state of + * the Registration Lifecycle by node value. + */ +async function makeFirstRegistration( + context: Context, + { + subregistryId, + node, + expiresAt, + }: { + subregistryId: AccountId; + node: Node; + expiresAt: UnixTimestamp; + }, +) { + return context.db.insert(schema.registrationLifecycle).values({ + subregistryId: serializeAccountId(subregistryId), + node, + expiresAt: BigInt(expiresAt), + }); +} + +/** + * Make subsequent registration + * + * Updates the current state of the Registration Lifecycle by node value. + */ +async function makeSubsequentRegistration( + context: Context, + { + node, + expiresAt, + }: { + node: Node; + expiresAt: UnixTimestamp; + }, +) { + return context.db + .update(schema.registrationLifecycle, { node }) + .set({ expiresAt: BigInt(expiresAt) }); +} + +/** + * Extend registration + * + * Updates the current state of the Registration Lifecycle by node value. + */ +async function extendRegistration( + context: Context, + { + node, + expiresAt, + }: { + node: Node; + expiresAt: UnixTimestamp; + }, +) { + return context.db + .update(schema.registrationLifecycle, { node }) + .set({ expiresAt: BigInt(expiresAt) }); +} + +export async function initializeRegistrarActionRegistration( + context: Context, + { + id, + registrationLifecycle, + registrant, + block, + transactionHash, + eventIds, + }: { + id: RegistrarAction["id"]; + registrationLifecycle: RegistrarAction["registrationLifecycle"]; + registrant: RegistrarAction["registrant"]; + block: RegistrarAction["block"]; + transactionHash: RegistrarAction["transactionHash"]; + eventIds: RegistrarAction["eventIds"]; + }, + { expiresAt }: { expiresAt: UnixTimestamp }, +) { + const { node, subregistry } = registrationLifecycle; + const { subregistryId } = subregistry; + const type = RegistrarActionTypes.Renewal; + + // 1. Create logical event key + const logicalEventKey = makeLogicalEventKey({ + node, + subregistryId, + transactionHash, + }); + + // 2. Store mapping between logical event key and logical event id + await context.db.insert(schema.tempLogicalSubregistryAction).values({ + logicalEventKey, + logicalEventId: id, + }); + + // 3. Calculate incremental duration + const currentRegistrationLifecycle = await getRegistrationLifecycle(context, { + node, + }); + + if (!currentRegistrationLifecycle) { + throw new Error( + `Current Registration Lifecycle record was not found for node '${registrationLifecycle.node}'`, + ); + } + + const incrementalDuration = deserializeDuration(expiresAt - block.timestamp); + + // 4. Store initial record for the "logical" Registrar Action + await context.db.insert(schema.registrarAction).values({ + id, + subregistryId: serializeAccountId(subregistryId), + type, + node, + incrementalDuration: BigInt(incrementalDuration), + registrant, + blockNumber: BigInt(block.number), + blockTimestamp: BigInt(block.timestamp), + transactionHash, + eventIds, + }); +} + +export async function initializeRegistrarActionRenewal( + context: Context, + { + id, + registrationLifecycle, + registrant, + block, + transactionHash, + eventIds, + }: { + id: RegistrarAction["id"]; + registrationLifecycle: RegistrarAction["registrationLifecycle"]; + registrant: RegistrarAction["registrant"]; + block: RegistrarAction["block"]; + transactionHash: RegistrarAction["transactionHash"]; + eventIds: RegistrarAction["eventIds"]; + }, + { expiresAt }: { expiresAt: UnixTimestamp }, +) { + const { node, subregistry } = registrationLifecycle; + const { subregistryId } = subregistry; + const type = RegistrarActionTypes.Renewal; + + // 1. Create logical event key + const logicalEventKey = makeLogicalEventKey({ + node, + subregistryId, + transactionHash, + }); + + // 2. Store mapping between logical event key and logical event id + await context.db.insert(schema.tempLogicalSubregistryAction).values({ + logicalEventKey, + logicalEventId: id, + }); + + // 3. Calculate incremental duration + const currentRegistrationLifecycle = await getRegistrationLifecycle(context, { + node, + }); + + if (!currentRegistrationLifecycle) { + throw new Error( + `Current Registration Lifecycle record was not found for node '${registrationLifecycle.node}'`, + ); + } + + const incrementalDuration = deserializeDuration( + expiresAt - bigIntToNumber(currentRegistrationLifecycle.expiresAt), + ); + + // 4. Store initial record for the "logical" Registrar Action + await context.db.insert(schema.registrarAction).values({ + id, + subregistryId: serializeAccountId(subregistryId), + type, + node, + incrementalDuration: BigInt(incrementalDuration), + registrant, + blockNumber: BigInt(block.number), + blockTimestamp: BigInt(block.timestamp), + transactionHash, + eventIds, + }); +} + +/** + * Handle registration event + */ +export async function handleRegistration( + context: Context, + event: Event, + { + subregistryId, + node, + expiresAt, + registrant, + }: { + subregistryId: AccountId; + node: Node; + expiresAt: UnixTimestamp; + registrant: Address; + }, +) { + // 0. Handle possible subsequent registration. + // Get the state of a possibly indexed registration record for this node + // before this registration occurred. + const currentRegistrationLifecycle = await getRegistrationLifecycle(context, { node }); + + if (currentRegistrationLifecycle) { + // 1. If a RegistrationLifecycle for the `node` has been already indexed, + // it means that another RegistrationLifecycle was made for the `node` after + // the previously indexed RegistrationLifecycle expired and its grace period ended. + await makeSubsequentRegistration(context, { node, expiresAt }); + } else { + // 1. It's a first-time registration made for the `node` value. + await makeFirstRegistration(context, { + subregistryId, + node, + expiresAt, + }); + } + + // 2. Initialize the "logical" Registrar Action record for Registration + const subregistry = await getSubregistry(context, { subregistryId }); + + // Invariant: subregistry record must exist + if (!subregistry) { + throw new Error(`Subregistry record must exists for '${serializeAccountId(subregistryId)}.'`); + } + + await initializeRegistrarActionRegistration( + context, + { + id: event.id, + registrationLifecycle: { + expiresAt, + node, + subregistry: { + subregistryId, + node: subregistry.node, + }, + }, + registrant, + block: { + number: bigIntToNumber(event.block.number), + timestamp: bigIntToNumber(event.block.timestamp), + }, + transactionHash: event.transaction.hash, + eventIds: [event.id], + }, + { + expiresAt, + }, + ); +} + +/** + * Handle Renewal + */ +export async function handleRenewal( + context: Context, + event: Event, + { + subregistryId, + node, + expiresAt, + registrant, + }: { + subregistryId: AccountId; + node: Node; + expiresAt: UnixTimestamp; + registrant: Address; + }, +) { + // TODO: 0. enforce an invariant that for Renewal actions, + // the registration must be in a "renewable" state. + // We can't add the state invariant about name renewals yet, because + // doing so would require us to index more historical RegistrarControllers + + // 1. Initialize the "logical" Registrar Action record for Renewal + const subregistry = await getSubregistry(context, { subregistryId }); + + // Invariant: subregistry record must exist + if (!subregistry) { + throw new Error(`Subregistry record must exists for '${serializeAccountId(subregistryId)}.'`); + } + + await initializeRegistrarActionRenewal( + context, + { + id: event.id, + registrationLifecycle: { + expiresAt, + node, + subregistry: { + subregistryId, + node: subregistry.node, + }, + }, + registrant, + block: { + number: bigIntToNumber(event.block.number), + timestamp: bigIntToNumber(event.block.timestamp), + }, + transactionHash: event.transaction.hash, + eventIds: [event.id], + }, + { + expiresAt, + }, + ); + + // 2. Extend Registration's expiry after creating the Registrar Action + // record. This is important for calculating incremental duration + // value correctly. + + await extendRegistration(context, { node, expiresAt }); +} diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts new file mode 100644 index 0000000000..81493afda2 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts @@ -0,0 +1,27 @@ +/** + * This file contains handlers used in event handlers for a Registrar contract. + */ + +import type { Context } from "ponder:registry"; +import schema from "ponder:schema"; + +import { type AccountId, type Node, serializeAccountId } from "@ensnode/ensnode-sdk"; + +export async function upsertSubregistry( + context: Context, + { + subregistryId, + node, + }: { + subregistryId: AccountId; + node: Node; + }, +) { + await context.db + .insert(schema.subregistry) + .values({ + subregistryId: serializeAccountId(subregistryId), + node, + }) + .onConflictDoNothing(); +} diff --git a/packages/ensnode-schema/src/schemas/registrars.schema.ts b/packages/ensnode-schema/src/schemas/registrars.schema.ts index 672a89cb97..9e05cf8b67 100644 --- a/packages/ensnode-schema/src/schemas/registrars.schema.ts +++ b/packages/ensnode-schema/src/schemas/registrars.schema.ts @@ -355,7 +355,7 @@ export const registrationLifecycleRelations = relations(registrationLifecycle, ( * * - exactly one Registration Lifecycle */ -export const logicalRegistrarActionRelations = relations(registrarAction, ({ one }) => ({ +export const registrarActionRelations = relations(registrarAction, ({ one }) => ({ registrationLifecycle: one(registrationLifecycle, { fields: [registrarAction.node], references: [registrationLifecycle.node], diff --git a/packages/ensnode-sdk/src/ensindexer/config/types.ts b/packages/ensnode-sdk/src/ensindexer/config/types.ts index 9f4f680383..18dbb2034b 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/types.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/types.ts @@ -14,6 +14,7 @@ export enum PluginName { ThreeDNS = "threedns", ProtocolAcceleration = "protocol-acceleration", Referrals = "referrals", + Registrars = "registrars", TokenScope = "tokenscope", } From 5de8777186e44463dfd8d3e91a215087e356eb9b Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 6 Nov 2025 09:15:00 +0100 Subject: [PATCH 06/13] feat(ensnode-sdk): create `datetime` module Hosts functionality for working with dates and time. --- packages/ensnode-sdk/src/shared/datetime.test.ts | 16 ++++++++++++++++ packages/ensnode-sdk/src/shared/datetime.ts | 9 +++++++++ packages/ensnode-sdk/src/shared/index.ts | 1 + 3 files changed, 26 insertions(+) create mode 100644 packages/ensnode-sdk/src/shared/datetime.test.ts create mode 100644 packages/ensnode-sdk/src/shared/datetime.ts diff --git a/packages/ensnode-sdk/src/shared/datetime.test.ts b/packages/ensnode-sdk/src/shared/datetime.test.ts new file mode 100644 index 0000000000..6f97205e74 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/datetime.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; + +import { durationBetween } from "./datetime"; + +describe("datetime", () => { + describe("durationBetween()", () => { + it("returns duration for valid input where start is before end", () => { + expect(durationBetween(1234, 4321)).toEqual(3087); + }); + it("throws an error for invalid input where end is before start", () => { + expect(() => durationBetween(4321, 1234)).toThrowError( + /Duration must be a non-negative integer/i, + ); + }); + }); +}); diff --git a/packages/ensnode-sdk/src/shared/datetime.ts b/packages/ensnode-sdk/src/shared/datetime.ts new file mode 100644 index 0000000000..870b05af26 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/datetime.ts @@ -0,0 +1,9 @@ +import { deserializeDuration } from "./deserialize"; +import type { Duration, UnixTimestamp } from "./types"; + +/** + * Duration between two moments in time. + */ +export function durationBetween(start: UnixTimestamp, end: UnixTimestamp): Duration { + return deserializeDuration(end - start, "Duration"); +} diff --git a/packages/ensnode-sdk/src/shared/index.ts b/packages/ensnode-sdk/src/shared/index.ts index c5712bc189..6dde8f001a 100644 --- a/packages/ensnode-sdk/src/shared/index.ts +++ b/packages/ensnode-sdk/src/shared/index.ts @@ -4,6 +4,7 @@ export * from "./address"; export * from "./cache"; export * from "./collections"; export * from "./currencies"; +export * from "./datetime"; export { deserializeBlockNumber, deserializeBlockRef, From 0de2587e99c61ce5822feecccd7ef41a01298842 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 6 Nov 2025 09:15:29 +0100 Subject: [PATCH 07/13] feat(ensnode-schema): improve docs --- .../src/schemas/registrars.schema.ts | 344 ++++++++++++------ .../ensnode-sdk/src/registrars/subregistry.ts | 6 +- 2 files changed, 231 insertions(+), 119 deletions(-) diff --git a/packages/ensnode-schema/src/schemas/registrars.schema.ts b/packages/ensnode-schema/src/schemas/registrars.schema.ts index 9e05cf8b67..67baf9080e 100644 --- a/packages/ensnode-schema/src/schemas/registrars.schema.ts +++ b/packages/ensnode-schema/src/schemas/registrars.schema.ts @@ -5,14 +5,19 @@ import { index, onchainEnum, onchainTable, relations, uniqueIndex } from "ponder"; /** - * Subregistry + * Subregistries + * + * @see https://ensnode.io/docs/reference/terminology/#subregistry */ -export const subregistry = onchainTable( +export const subregistries = onchainTable( "subregistries", (t) => ({ /** * Subregistry ID * + * Identifies the chainId and address of the smart contract associated + * with the subregistry. + * * Guaranteed to be a string formatted according to the CAIP-10 standard. * * @see https://chainagnostic.org/CAIPs/caip-10 @@ -20,7 +25,8 @@ export const subregistry = onchainTable( subregistryId: t.text().primaryKey(), /** - * The node of a name the subregistry manages. Example managed names: + * The node (namehash) of the name the subregistry manages subnames of. + * Example subregistry managed names: * - `eth` * - `base.eth` * - `linea.eth` @@ -35,15 +41,41 @@ export const subregistry = onchainTable( ); /** - * Registration Lifecycle + * Registration Lifecycles + * + * A "registration lifecycle" represents a single cycle of a name being + * registered once followed by renewals (expiry date extensions) any number of + * times. + * + * Note that this data model only tracks the *most recently created* + * "registration lifecycle" record for a name and doesn't track + * *all* "registration lifecycle" records for a name across time. + * Therefore, if a name goes through multiple cycles of: + * (registration -> expiry -> release) -> + * (registration -> expiry -> release) -> etc.. + * this data model only stores data of the most recently created + * "registration lifecycle". + * + * For now we make the following simplifying assumptions: + * 1. That no two subregistries hold state for the same node. + * 2. That the subregistry associated with the name X in the ENS root registry + * exclusively holds state for subnames of X. + * + * These simplifying assumptions happen to be true for the scope of our + * current indexing logic, but nothing in the ENS protocol fundamentally + * forces this to always be true. Therefore this data model will need + * refactoring in the future as our indexing logic expands to handle + * more complex scenarios. */ -export const registrationLifecycle = onchainTable( +export const registrationLifecycles = onchainTable( "registration_lifecycles", (t) => ({ /** - * The node of the FQDN of the domain this is associated with, - * guaranteed to be a subname of the associated subregistry - * for which the registration was executed. + * The node (namehash) of the FQDN of the domain the registration lifecycle + * is associated with. + * + * Guaranteed to be a subname of the node (namehash) of the subregistry + * identified by `subregistryId`. * * Guaranteed to be a hex string representation of 32-bytes. */ @@ -52,6 +84,9 @@ export const registrationLifecycle = onchainTable( /** * Subregistry ID * + * Identifies the chainId and address of the subregistry smart contract + * that manages the registration lifecycle. + * * Guaranteed to be a string formatted according to the CAIP-10 standard. * * @see https://chainagnostic.org/CAIPs/caip-10 @@ -59,7 +94,9 @@ export const registrationLifecycle = onchainTable( subregistryId: t.text().notNull(), /** - * Unix timestamp when Registration Lifecycle is scheduled to expire. + * Expires at + * + * Unix timestamp when the Registration Lifecycle is scheduled to expire. */ expiresAt: t.bigint().notNull(), }), @@ -79,53 +116,69 @@ export const registrarActionType = onchainEnum("registrar_action_type", [ ]); /** - * "Logical" Registrar Action + * Logical Registrar Actions * - * Represents a "logical" RegistrarAction, but the state recorded for - * a "logical" RegistrarAction may be an aggregate across multiple onchain - * events that may be distributed across multiple contracts (such as - * a RegistrarController and its associated BaseRegistrar) within - * a single transaction. + * This table models "logical actions" rather than "events" because a single + * "logical action", such as a single registration or renewal, may emit + * multiple onchain events from multiple contracts where each of those + * individual events may only provide a subset of the data about the full + * "logical action". Therefore, here we aggregate data about each + * "logical action" that may be sourced from multiple onchain events from + * multiple contracts. * - * Consider the following situation: - * 1) When someone makes a new registration, multiple contracts take part in - * the registration process. - * 2) In order to build a single "logical" Registrar Action record, - * we may need information from one or more events. For example, - * the `NameRegistered` event from the BaseRegistrar contract includes - * information for fields like: + * Each "logical action" in this table is associated with a single transaction. + * However, it should be noted that a single transaction may perform any number + * of "logical actions". + * + * For example, consider the "logical registrar action" of registering a direct + * subname of .eth. This "logical action" spans interactions across multiple + * contracts that emit multiple onchain events: + * + * 1. The "EthBaseRegistrar" contract emits a `NameRegistered` event enabling + * the tracking of data including: * - `node` * - `incrementalDuration` * - `registrant` - * We use this event to initiate the "logical" Registrar Action record. - * - * Another event we may index (and in most cases we do) is - * `NameRegistered` from RegistrarController contract, which may include: + * 2. A "RegistrarController" contract emits its own `NameRegistered` event + * enabling the tracking of data including: * - `baseCost` * - `premium` * - `total` * - `encodedReferrer` - * We use this event to update the "logical" Registrar Action record. * - * Both of those events contribute to a single "logical" Registrar Action record. + * Here we aggregate the state from both of these events into a single + * "logical registrar action". */ -export const registrarAction = onchainTable( - "registrar_action", +export const registrarActions = onchainTable( + "registrar_actions", (t) => ({ /** * "Logical" Registrar Action ID * - * The `id` value is a deterministic identifier for the initial onchain event - * associated with the "logical" RegistrarAction. + * The `id` value is a deterministic and globally unique identifier for + * the "logical registrar action". + * + * The `id` value represents the *initial* onchain event associated with + * the "logical registrar action", but the full state of + * the "logical registrar action" is an aggregate across each of + * the onchain events referenced in the `eventIds` field. * * Guaranteed to be the very first element in `eventIds` array. */ id: t.text().primaryKey(), + /** + * The type of the "logical registrar action". + */ + type: registrarActionType().notNull(), + /** * Subregistry ID * - * The ID of the subregistry which executed the "logical" Registrar Action. + * The ID of the subregistry the "logical registrar action" was taken on. + * + * Identifies the chainId and address of the associated subregistry smart + * contract. * * Guaranteed to be a string formatted according to the CAIP-10 standard. * @@ -134,72 +187,95 @@ export const registrarAction = onchainTable( subregistryId: t.text().notNull(), /** - * The node (namehash) of the name associated with the "logical" Registrar - * Action. + * The node (namehash) of the FQDN of the domain associated with + * the "logical registrar action". * * Guaranteed to be a hex string representation of 32-bytes. */ node: t.hex().notNull(), - /** - * Type of the "logical" Registrar Action. - */ - type: registrarActionType().notNull(), - /** * Incremental Duration * - * Definition of "incremental duration" is - * the incremental increase in the lifespan of the registration for - * `node` that was active as of `blockTimestamp`. + * If `type` is "registration": + * - Represents the duration between `blockTimestamp` and + * the initial `expiresAt` value that the associated + * "registration lifecycle" will be initialized with. + * If `type` is "renewal": + * - Represents the incremental increase in duration made to + * the `expiresAt` value in the associated "registration lifecycle". + * + * A "registration lifecycle" may be extended via renewal even after it + * expires if it is still within its grace period. + * + * Consider the following scenario: * - * Please consider the following situation: + * The "registration lifecycle" of a direct subname of .eth is scheduled to + * expire on Jan 1, midnight UTC. It is currently 30 days after this + * expiration time. Therefore, there are currently another 60 days of grace + * period remaining for this name. Anyone can still make a renewal to + * extend the "registration lifecycle" of this name. * - * A registration of direct subname of .eth name is scheduled to expire on - * Jan 1, midnight UTC. It is currently 30 days after this expiration time. - * Therefore, there are currently another 60 days of grace period remaining - * for this name. Anyone can now make a renewal of this name. + * Given this scenario, consider the following examples: * - * There are two possible scenarios when a renewal is made: + * 1. If a renewal is made with 10 days incremental duration, + * the "registration lifecycle" for this name will remain in + * an "expired" state, but it will now have another 70 days of + * grace period remaining. * - * 1) If a renewal is made for 10 days incremental duration, - * this name remains in an "expired" state, but it now - * has another 70 days of grace period remaining. + * 2. If a renewal is made with 50 days incremental duration, + * the "registration lifecycle" for this name will no longer be + * "expired" and will become "active", but the "registration lifecycle" + * will now be scheduled to expire again in 20 days. * - * 2) If a renewal is made for 50 days incremental duration, - * this name is no longer "expired" and is active, but it now - * expires in 20 days. + * After the "registration lifecycle" for a name becomes expired by more + * than its grace period, it can no longer be renewed by anyone and is + * considered "released". The name must first be registered again, starting + * a new "registration lifecycle" of + * active / expired / grace period / released. * - * After the latest registration of a direct subname becomes expired by - * more than the grace period, it can no longer be renewed by anyone. - * It must first be registered again, starting a new registration lifecycle of - * expiry / grace period / etc. + * May be 0. * * Guaranteed to be a non-negative bigint value. */ incrementalDuration: t.bigint().notNull(), /** - * Base cost of the "logical" Registrar Action. + * Base cost + * + * Base cost (before any `premium`) of Ether measured in units of Wei + * paid to execute the "logical registrar action". + * + * May be 0. * * Guaranteed to be: * 1) null if and only if `total` is null. - * 2) Otherwise, a non-negative bigint value for registrations. + * 2) Otherwise, a non-negative bigint value. */ baseCost: t.bigint(), /** - * Premium of the "logical" Registrar Action. + * Premium + * + * "premium" cost (in excesses of the `baseCost`) of Ether measured in + * units of Wei paid to execute the "logical registrar action". + * + * May be 0. * * Guaranteed to be: * 1) null if and only if `total` is null. * 2) Otherwise, zero when `type` is `renewal`. - * 3) Otherwise, a non-negative bigint value `type` is `registration`. + * 3) Otherwise, a non-negative bigint value. */ premium: t.bigint(), /** - * Total cost of performing the "logical" Registrar Action. + * Total + * + * Total cost of Ether measured in units of Wei paid to execute + * the "logical registrar action". + * + * May be 0. * * Guaranteed to be: * 1) null if and only if both `baseCost` and `premium` are null. @@ -209,10 +285,15 @@ export const registrarAction = onchainTable( total: t.bigint(), /** - * Account that initiated the "logical" Registrar Action and - * is paying the `total` cost. + * Registrant + * + * Refers to address on the same `chainId` as referred by `subregistryId` + * that initiated the "logical" Registrar Action and is paying + * the `total` cost. + * + * Guaranteed to be a string formatted according to the CAIP-10 standard. */ - registrant: t.hex().notNull(), + registrant: t.text().notNull(), /** * Encoded Referrer @@ -220,12 +301,8 @@ export const registrarAction = onchainTable( * Represents the "raw" 32-byte "referrer" value emitted onchain in * association with the registrar action. * - * If a registrar / registrar controller doesn't support the concept of - * referrers then this field is set to null. - * * Guaranteed to be: - * 1) null if a registrar / registrar controller doesn't support - * the concept of referrers. + * 1) null if the emitted `eventIds` contain no information about a referrer. * 2) Otherwise, a hex string representation of 32-bytes. */ encodedReferrer: t.hex(), @@ -233,6 +310,11 @@ export const registrarAction = onchainTable( /** * Decoded referrer * + * Decoded referrer according to the subjective interpretation of + * `encodedReferrer` defined for ENS Holiday Awards. + * + * Refers to address on the same `chainId` as referred by `subregistryId`. + * * Guaranteed to be: * 1) null if `encodedReferrer` is null. * 2) Otherwise, a valid EVM address (including zero address). @@ -248,14 +330,15 @@ export const registrarAction = onchainTable( /** * Timestamp of the block that includes the "logical" Registrar Action. - * - * Guaranteed to be a non-negative bigint value. */ - blockTimestamp: t.bigint().notNull(), + timestamp: t.bigint().notNull(), /** * Transaction hash of the transaction on `chainId` chain associated with - * the "logical" Registrar Action. + * the Logical Registrar Action. + * + * Note that a single transaction may be associated with any number of + * "Logical" Registrar Actions. * * Guaranteed to be a string representation of 32-bytes. */ @@ -264,18 +347,29 @@ export const registrarAction = onchainTable( /** * Event IDs * - * An array of IDs referencing all onchain events, ordered by logIndex - * that have ever contributed to the state of the "logical" Registrar Action. - * - * For example, the IDs will: - * 1) Always reference event emitted by BaseRegistrar contract. - * 2) Optionally reference event emitted by Registrar Controller contract, - * if and only if the given Registrar Controller contract is indexed. - * - * Note: Some Registrar Controller contracts that are not indexed - *. as they remain unknown to ENSIndexer at the moment. - * - * The `id` value is guaranteed to be the initial element of that array. + * Array of the eventIds that have contributed to the state of + * the Logical Registrar Action. + * + * Each eventId is a deterministic and globally unique onchain event + * identifier. + * + * Guarantees: + * - Each eventId is of events that occurred on `chainId` within + * `blockNumber`. + * - At least 1 eventId. + * - Ordered chronologically (ascending) by logIndex within `blockNumber` + * on `chainId`. + * - The first element in the array is equal to the `id` of + * the "logical registrar action". + * + * The following ideas are not generalized for ENS overall but happen to + * be a characteristic of the scope of our current indexing logic: + * 1. These id's always reference events emitted by + * a related "BaseRegistrar" contract. + * 2. These id's optionally reference events emitted by + * a related "Registrar Controller" contract. This is because our + * current indexing logic doesn't guarantee to index + * all "Registrar Controller" contracts. * * Guaranteed to: * - Reference at least one event. @@ -286,24 +380,28 @@ export const registrarAction = onchainTable( (t) => ({ byRegistrant: index().on(t.registrant), byDecodedReferrer: index().on(t.decodedReferrer), - byBlockTimestamp: index().on(t.blockTimestamp), + byTimestamp: index().on(t.timestamp), }), ); /** - * "Logical" Subregistry Action Metadata + * Logical Subregistry Action Metadata * - * Building a single "logical" Subregistry Action requires data from multiple - * onchain events. While handling the first event, we create a temporary - * "Logical" Subregistry Action Metadata record where we store `logicalEventId`. + * NOTE: This table is an internal implementation detail of ENSIndexer and + * should not be queried outside of ENSIndexer. * - * The `logicalEventId` is used by subsequent event handlers to update - * the "logical" Subregistry Action record. In order to get `logicalEventId`, - * an event handler creates `logicalEventKey` from the currently handled - * onchain event. + * Building a "logical subregistry action" record may require data from + * multiple onchain events. To help aggregate data from multiple events into + * a single "logical subregistry action" ENSIndexer may temporarily store data + * here to achieve this data aggregation. * - * The very last event handler must remove the record referenced with - * `logicalEventKey` value. + * Note how multiple "logical subregistry actions" may be taken on + * the same `node` in the same `transactionHash`. For example, consider + * a case of a single transaction registering a name and subsequently renewing + * it twice. While this may be silly it is technically possible and therefore + * such cases must be considered. To support such cases, when + * the last event handler for a "logical subregistry action" has completed its + * processing the record referenced by the `logicalEventKey` must be removed. */ export const tempLogicalSubregistryAction = onchainTable("_subregistry_action_metadata", (t) => ({ /** @@ -317,9 +415,11 @@ export const tempLogicalSubregistryAction = onchainTable("_subregistry_action_me /** * Logical Event ID * - * A string holding the ID value to an existing "logical" Registrar Action - * record that was inserted while e use this event to initiate - * the "logical" Registrar Action record. + * A string holding the `id` value of the existing "logical registrar action" + * record that is currently being built as an aggregation of onchain events. + * + * May be used by subsequent event handlers to identify which + * "logical registrar action" to aggregate additional indexed state into. */ logicalEventId: t.text().notNull(), })); @@ -329,35 +429,43 @@ export const tempLogicalSubregistryAction = onchainTable("_subregistry_action_me /** * Subregistry Relations * - * - many RegistrationLifecycles + * Each Subregistry is related to: + * - 0 or more RegistrationLifecycles */ -export const subregistryRelations = relations(subregistry, ({ many }) => ({ - registrationLifecycle: many(registrationLifecycle), +export const subregistryRelations = relations(subregistries, ({ many }) => ({ + registrationLifecycle: many(registrationLifecycles), })); /** * Registration Lifecycle Relations * + * Each Registration Lifecycle is related to: * - exactly one Subregistry - * - many "logical" RegistrarActions + * - 0 or more "logical" RegistrarActions */ -export const registrationLifecycleRelations = relations(registrationLifecycle, ({ one, many }) => ({ - subregistry: one(subregistry, { - fields: [registrationLifecycle.subregistryId], - references: [subregistry.subregistryId], - }), +export const registrationLifecycleRelations = relations( + registrationLifecycles, + ({ one, many }) => ({ + subregistry: one(subregistries, { + fields: [registrationLifecycles.subregistryId], + references: [subregistries.subregistryId], + }), - registrarAction: many(registrarAction), -})); + registrarAction: many(registrarActions), + }), +); /** * "Logical" Registrar Action Relations * - * - exactly one Registration Lifecycle + * Each "logical" Registrar Action is related to: + * - exactly one Registration Lifecycle (note the docs on + * Registration Lifecycle explaining how these records may + * be recycled across time). */ -export const registrarActionRelations = relations(registrarAction, ({ one }) => ({ - registrationLifecycle: one(registrationLifecycle, { - fields: [registrarAction.node], - references: [registrationLifecycle.node], +export const registrarActionRelations = relations(registrarActions, ({ one }) => ({ + registrationLifecycle: one(registrationLifecycles, { + fields: [registrarActions.node], + references: [registrationLifecycles.node], }), })); diff --git a/packages/ensnode-sdk/src/registrars/subregistry.ts b/packages/ensnode-sdk/src/registrars/subregistry.ts index 70016dfd92..d0b3b61a5a 100644 --- a/packages/ensnode-sdk/src/registrars/subregistry.ts +++ b/packages/ensnode-sdk/src/registrars/subregistry.ts @@ -7,11 +7,15 @@ import type { AccountId } from "../shared"; export interface Subregistry { /** * Subregistry Account ID + * + * Identifies the account of the smart contract associated + * with the subregistry. */ subregistryId: AccountId; /** - * The node of a name the subregistry manages. Example managed names: + * The node (namehash) of the name the subregistry manages subnames of. + * Example subregistry managed names: * - `eth` * - `base.eth` * - `linea.eth` From 25e35b909ce00f26e88318ca68da20c04c1a9b18 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 6 Nov 2025 09:16:48 +0100 Subject: [PATCH 08/13] refactor(ensindexer): update registrars plugin Favour simple business logic functions. Convert types, if possible, at the edge (ponder event handlers). --- .../basenames/handlers/Basenames_Registrar.ts | 47 ++- .../handlers/Basenames_RegistrarController.ts | 30 +- .../ethnames/handlers/Ethnames_Registrar.ts | 32 +- .../handlers/Ethnames_RegistrarController.ts | 38 +- .../handlers/Lineanames_Registrar.ts | 32 +- .../Lineanames_RegistrarController.ts | 24 +- .../registrars/shared/lib/registrar-action.ts | 119 ++++++ .../shared/lib/registrar-controller-events.ts | 81 ++-- .../registrars/shared/lib/registrar-events.ts | 367 ++++-------------- .../shared/lib/registration-lifecycle.ts | 88 +++++ .../registrars/shared/lib/subregistry.ts | 19 +- 11 files changed, 502 insertions(+), 375 deletions(-) create mode 100644 apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts create mode 100644 apps/ensindexer/src/plugins/registrars/shared/lib/registration-lifecycle.ts 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 3f05858176..77af19ccf3 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts @@ -4,7 +4,7 @@ import { ponder } from "ponder:registry"; import { namehash } from "viem/ens"; import { DatasourceNames } from "@ensnode/datasources"; -import { bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; +import { type BlockRef, bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; import { getDatasourceContract } from "@/lib/datasource-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; @@ -34,18 +34,27 @@ export default function () { 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 expiresAt = bigIntToNumber(event.args.expires); 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 handleRegistration(context, event, { + await handleRegistration(context, { + id, subregistryId, node, - expiresAt, registrant, + expiresAt, + block, + transactionHash, }); }, ); @@ -53,18 +62,27 @@ export default function () { 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 expiresAt = bigIntToNumber(event.args.expires); 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 handleRegistration(context, event, { + await handleRegistration(context, { + id, subregistryId, node, - expiresAt, registrant, + expiresAt, + block, + transactionHash, }); }, ); @@ -72,16 +90,25 @@ export default function () { 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 expiresAt = bigIntToNumber(event.args.expires); 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 handleRenewal(context, event, { + await handleRenewal(context, { + id, subregistryId, node, - expiresAt, registrant, + expiresAt, + block, + transactionHash, }); }, ); 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 fc0fc14575..6278486c8b 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts @@ -57,14 +57,18 @@ 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 transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -76,14 +80,18 @@ 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 transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -91,14 +99,18 @@ 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 transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -110,14 +122,18 @@ 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 transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -125,14 +141,18 @@ 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 transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); 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 f831c6ae11..1d3691e103 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts @@ -4,7 +4,7 @@ import { ponder } from "ponder:registry"; import { namehash } from "viem/ens"; import { DatasourceNames } from "@ensnode/datasources"; -import { bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; +import { type BlockRef, bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; import { getDatasourceContract } from "@/lib/datasource-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; @@ -33,18 +33,27 @@ export default function () { 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 expiresAt = bigIntToNumber(event.args.expires); 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 handleRegistration(context, event, { + await handleRegistration(context, { + id, subregistryId, node, - expiresAt, registrant, + expiresAt, + block, + transactionHash, }); }, ); @@ -52,16 +61,25 @@ export default function () { ponder.on( namespaceContract(pluginName, "Ethnames_BaseRegistrar:NameRenewed"), async ({ context, event }) => { + const id = event.id; const labelHash = tokenIdToLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, parentNode); - const expiresAt = bigIntToNumber(event.args.expires); 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 handleRenewal(context, event, { + await handleRenewal(context, { + id, subregistryId, node, - expiresAt, registrant, + expiresAt, + block, + transactionHash, }); }, ); 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 1114c3101d..5d58e5f137 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts @@ -41,6 +41,7 @@ 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); @@ -66,11 +67,15 @@ export default function () { decodedReferrer: null, } satisfies RegistrarActionReferralNotApplicable; - await handleRegistrarControllerEvent(context, event, { + const transactionHash = event.transaction.hash; + + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -78,6 +83,7 @@ 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); @@ -105,11 +111,15 @@ export default function () { decodedReferrer: null, } satisfies RegistrarActionReferralNotApplicable; - await handleRegistrarControllerEvent(context, event, { + const transactionHash = event.transaction.hash; + + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -121,8 +131,10 @@ 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 transactionHash = event.transaction.hash; /** * Ethnames_WrappedEthRegistrarController implements premiums, and base cost. @@ -145,11 +157,13 @@ export default function () { decodedReferrer: null, } satisfies RegistrarActionReferralNotApplicable; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -157,8 +171,10 @@ 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 transactionHash = event.transaction.hash; /** * Ethnames_WrappedEthRegistrarController implements premiums, and base cost. @@ -183,11 +199,13 @@ export default function () { decodedReferrer: null, } satisfies RegistrarActionReferralNotApplicable; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -199,8 +217,10 @@ 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 transactionHash = event.transaction.hash; /** * Ethnames_UnwrappedEthRegistrarController implements premiums, and base cost. @@ -226,11 +246,13 @@ export default function () { decodedReferrer, } satisfies RegistrarActionReferralAvailable; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -238,8 +260,10 @@ 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 transactionHash = event.transaction.hash; /** * Ethnames_UnwrappedEthRegistrarController implements premiums, and base cost. @@ -267,11 +291,13 @@ export default function () { decodedReferrer, } satisfies RegistrarActionReferralAvailable; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); 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 8f1d8e2cd2..e49c050706 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts @@ -4,7 +4,7 @@ import { ponder } from "ponder:registry"; import { namehash } from "viem/ens"; import { DatasourceNames } from "@ensnode/datasources"; -import { bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; +import { type BlockRef, bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; import { getDatasourceContract } from "@/lib/datasource-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; @@ -33,18 +33,27 @@ export default function () { 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 expiresAt = bigIntToNumber(event.args.expires); 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 handleRegistration(context, event, { + await handleRegistration(context, { + id, subregistryId, node, - expiresAt, registrant, + expiresAt, + block, + transactionHash, }); }, ); @@ -52,16 +61,25 @@ export default function () { ponder.on( namespaceContract(pluginName, "Lineanames_BaseRegistrar:NameRenewed"), async ({ context, event }) => { + const id = event.id; const labelHash = tokenIdToLabelHash(event.args.id); const node = makeSubdomainNode(labelHash, parentNode); - const expiresAt = bigIntToNumber(event.args.expires); 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 handleRenewal(context, event, { + await handleRenewal(context, { + id, subregistryId, node, - expiresAt, registrant, + expiresAt, + block, + transactionHash, }); }, ); 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 2684243775..e409bbb310 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts @@ -57,14 +57,18 @@ 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 transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -72,14 +76,18 @@ 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 transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -87,14 +95,18 @@ 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 transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); @@ -102,14 +114,18 @@ 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 transactionHash = event.transaction.hash; - await handleRegistrarControllerEvent(context, event, { + await handleRegistrarControllerEvent(context, { + id, subregistryId, node, pricing, referral, + transactionHash, }); }, ); diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts new file mode 100644 index 0000000000..7776713f09 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts @@ -0,0 +1,119 @@ +import type { Context } from "ponder:registry"; +import schema from "ponder:schema"; +import type { Hash } from "viem"; + +import { + type AccountId, + type Node, + type RegistrarAction, + serializeAccountId, +} from "@ensnode/ensnode-sdk"; + +/** + * Logical Event Key + * + * String formatted as: + * `{accountId}:{node}:{transactionHash}`, where `accountId` follows + * the CAIP-10 standard. + * + * @see https://chainagnostic.org/CAIPs/caip-10 + */ +export type LogicalEventKey = string; + +/** + * Make a logical event key for a "logical" registrar action. + */ +export function makeLogicalEventKey({ + subregistryId, + node, + transactionHash, +}: { + subregistryId: AccountId; + node: Node; + transactionHash: Hash; +}): LogicalEventKey { + return [serializeAccountId(subregistryId), node, transactionHash].join(":"); +} + +/** + * Get "logical" Registrar Action record by logical event key. + * + * @throws if the record cannot be found. + */ +export async function getLogicalRegistrarActionByEventKey( + context: Context, + logicalEventKey: LogicalEventKey, +) { + const tempRecord = await context.db.find(schema.tempLogicalSubregistryAction, { + logicalEventKey, + }); + + // Invariant: the "logical" Registrar Action ID must be available + if (!tempRecord) { + throw new Error( + `The required "logical" Registrar Action ID could not be found for the following logical event key: '${logicalEventKey}'.`, + ); + } + + const { logicalEventId } = tempRecord; + + const logicalRegistrarAction = await context.db.find(schema.registrarActions, { + id: logicalEventId, + }); + + // Invariant: the "logical" Registrar Action record must be available + if (!logicalRegistrarAction) { + throw new Error( + `The "logical" Registrar Action record, which could not be found for the following logical event ID: '${logicalEventId}'.`, + ); + } + + return logicalRegistrarAction; +} + +/** + * Initialize a record for the "logical" Registrar Action. + */ +export async function initializeRegistrarAction( + context: Context, + { + id, + type, + registrationLifecycle, + incrementalDuration, + registrant, + block, + transactionHash, + eventIds, + }: Omit, +) { + const { node, subregistry } = registrationLifecycle; + const { subregistryId } = subregistry; + + // 1. Create logical event key + const logicalEventKey = makeLogicalEventKey({ + node, + subregistryId, + transactionHash, + }); + + // 2. Store mapping between logical event key and logical event id + await context.db.insert(schema.tempLogicalSubregistryAction).values({ + logicalEventKey, + logicalEventId: id, + }); + + // 4. Store initial record for the "logical" Registrar Action + await context.db.insert(schema.registrarActions).values({ + id, + subregistryId: serializeAccountId(subregistryId), + type, + node, + incrementalDuration: BigInt(incrementalDuration), + registrant, + blockNumber: BigInt(block.number), + timestamp: BigInt(block.timestamp), + transactionHash, + eventIds, + }); +} diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts index 2ea1c75b16..3664c6db83 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts @@ -1,6 +1,6 @@ import type { Context, Event } from "ponder:registry"; import schema from "ponder:schema"; -import type { Address } from "viem"; +import type { Address, Hash } from "viem"; import { type AccountId, @@ -12,40 +12,7 @@ import { type RegistrarActionReferral, } from "@ensnode/ensnode-sdk"; -import { type LogicalEventKey, makeLogicalEventKey } from "../../shared/lib/registrar-events"; - -/** - * Get "logical" Registrar Action record by logical event key. - * - * @throws if the record cannot be found. - */ -async function getLogicalRegistrarAction(context: Context, logicalEventKey: LogicalEventKey) { - const tempRecord = await context.db.find(schema.tempLogicalSubregistryAction, { - logicalEventKey, - }); - - // Invariant: the "logical" Registrar Action ID must be available - if (!tempRecord) { - throw new Error( - `Handling Registrar Controller Registration action requires the "logical" Registrar Action ID, which could not be found for the following logical event key: '${logicalEventKey}'.`, - ); - } - - const { logicalEventId } = tempRecord; - - const logicalRegistrarAction = await context.db.find(schema.registrarAction, { - id: logicalEventId, - }); - - // Invariant: the "logical" Registrar Action record must be available - if (!logicalRegistrarAction) { - throw new Error( - `Handling Registrar Controller Registration action requires the "logical" Registrar Action record, which could not be found for the following logical event ID: '${logicalEventId}'.`, - ); - } - - return logicalRegistrarAction; -} +import { getLogicalRegistrarActionByEventKey, makeLogicalEventKey } from "./registrar-action"; /** * Update the "logical" Registrar Action: @@ -55,29 +22,37 @@ async function getLogicalRegistrarAction(context: Context, logicalEventKey: Logi */ export async function handleRegistrarControllerEvent( context: Context, - event: Event, { + id, subregistryId, node, pricing, referral, + transactionHash, }: { + id: Event["id"]; subregistryId: AccountId; node: Node; pricing: RegistrarActionPricing; referral: RegistrarActionReferral; + transactionHash: Hash; }, ) { + // 1. Make Logical Event Key const logicalEventKey = makeLogicalEventKey({ subregistryId, node, - transactionHash: event.transaction.hash, + transactionHash, }); - // get the "logical" Registrar Action to update - const { id } = await getLogicalRegistrarAction(context, logicalEventKey); + // 2. Use the Logical Event Key to get the "logical" Registrar Action record + // which needs to be updated. + const logicalRegistrarAction = await getLogicalRegistrarActionByEventKey( + context, + logicalEventKey, + ); - // get pricing info + // 3. Prepare pricing info let baseCost: bigint | null; let premium: bigint | null; let total: bigint | null; @@ -92,7 +67,7 @@ export async function handleRegistrarControllerEvent( total = null; } - // get referral info + // 4. Prepare referral info let encodedReferrer: EncodedReferrer | null; let decodedReferrer: Address | null; @@ -104,14 +79,18 @@ export async function handleRegistrarControllerEvent( decodedReferrer = null; } - // update pricing data & referral data accordingly - // plus, append new event id to `eventIds` - await context.db.update(schema.registrarAction, { id }).set((logicalRegistrarAction) => ({ - baseCost, - premium, - total, - encodedReferrer, - decodedReferrer, - eventIds: [...logicalRegistrarAction.eventIds, event.id], - })); + // 5. Update the "logical" Registrar Action record with + // - pricing data, + // - referral data + // - new event ID appended to `eventIds` + await context.db + .update(schema.registrarActions, { id: logicalRegistrarAction.id }) + .set(({ eventIds }) => ({ + baseCost, + premium, + total, + encodedReferrer, + decodedReferrer, + eventIds: [...eventIds, id], + })); } diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts index 6fade308d2..d6bbf86759 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts @@ -3,262 +3,49 @@ */ import type { Context, Event } from "ponder:registry"; -import schema from "ponder:schema"; import type { Address, Hash } from "viem"; import { type AccountId, + type BlockRef, bigIntToNumber, - deserializeDuration, + durationBetween, type Node, - type RegistrarAction, RegistrarActionTypes, serializeAccountId, type UnixTimestamp, } from "@ensnode/ensnode-sdk"; -/** - * Logical Event Key - * - * String formatted as: - * `{accountId}:{node}:{transactionHash}`, where `accountId` follows - * the CAIP-10 standard. - * - * @see https://chainagnostic.org/CAIPs/caip-10 - */ -export type LogicalEventKey = string; - -export function makeLogicalEventKey({ - subregistryId, - node, - transactionHash, -}: { - subregistryId: AccountId; - node: Node; - transactionHash: Hash; -}): LogicalEventKey { - return [serializeAccountId(subregistryId), node, transactionHash].join(":"); -} - -async function getSubregistry(context: Context, { subregistryId }: { subregistryId: AccountId }) { - return context.db.find(schema.subregistry, { subregistryId: serializeAccountId(subregistryId) }); -} - -async function getRegistrationLifecycle(context: Context, { node }: { node: Node }) { - return context.db.find(schema.registrationLifecycle, { node }); -} - -/** - * Make first registration - * - * Inserts a new record to track the current state of - * the Registration Lifecycle by node value. - */ -async function makeFirstRegistration( - context: Context, - { - subregistryId, - node, - expiresAt, - }: { - subregistryId: AccountId; - node: Node; - expiresAt: UnixTimestamp; - }, -) { - return context.db.insert(schema.registrationLifecycle).values({ - subregistryId: serializeAccountId(subregistryId), - node, - expiresAt: BigInt(expiresAt), - }); -} - -/** - * Make subsequent registration - * - * Updates the current state of the Registration Lifecycle by node value. - */ -async function makeSubsequentRegistration( - context: Context, - { - node, - expiresAt, - }: { - node: Node; - expiresAt: UnixTimestamp; - }, -) { - return context.db - .update(schema.registrationLifecycle, { node }) - .set({ expiresAt: BigInt(expiresAt) }); -} +import { initializeRegistrarAction } from "./registrar-action"; +import { + extendRegistrationLifecycle, + getRegistrationLifecycle, + makeFirstRegistration, + makeSubsequentRegistration, +} from "./registration-lifecycle"; +import { getSubregistry } from "./subregistry"; /** - * Extend registration - * - * Updates the current state of the Registration Lifecycle by node value. + * Handle registration event */ -async function extendRegistration( - context: Context, - { - node, - expiresAt, - }: { - node: Node; - expiresAt: UnixTimestamp; - }, -) { - return context.db - .update(schema.registrationLifecycle, { node }) - .set({ expiresAt: BigInt(expiresAt) }); -} - -export async function initializeRegistrarActionRegistration( +export async function handleRegistration( context: Context, { id, - registrationLifecycle, - registrant, - block, - transactionHash, - eventIds, - }: { - id: RegistrarAction["id"]; - registrationLifecycle: RegistrarAction["registrationLifecycle"]; - registrant: RegistrarAction["registrant"]; - block: RegistrarAction["block"]; - transactionHash: RegistrarAction["transactionHash"]; - eventIds: RegistrarAction["eventIds"]; - }, - { expiresAt }: { expiresAt: UnixTimestamp }, -) { - const { node, subregistry } = registrationLifecycle; - const { subregistryId } = subregistry; - const type = RegistrarActionTypes.Renewal; - - // 1. Create logical event key - const logicalEventKey = makeLogicalEventKey({ - node, subregistryId, - transactionHash, - }); - - // 2. Store mapping between logical event key and logical event id - await context.db.insert(schema.tempLogicalSubregistryAction).values({ - logicalEventKey, - logicalEventId: id, - }); - - // 3. Calculate incremental duration - const currentRegistrationLifecycle = await getRegistrationLifecycle(context, { node, - }); - - if (!currentRegistrationLifecycle) { - throw new Error( - `Current Registration Lifecycle record was not found for node '${registrationLifecycle.node}'`, - ); - } - - const incrementalDuration = deserializeDuration(expiresAt - block.timestamp); - - // 4. Store initial record for the "logical" Registrar Action - await context.db.insert(schema.registrarAction).values({ - id, - subregistryId: serializeAccountId(subregistryId), - type, - node, - incrementalDuration: BigInt(incrementalDuration), - registrant, - blockNumber: BigInt(block.number), - blockTimestamp: BigInt(block.timestamp), - transactionHash, - eventIds, - }); -} - -export async function initializeRegistrarActionRenewal( - context: Context, - { - id, - registrationLifecycle, registrant, + expiresAt, block, transactionHash, - eventIds, - }: { - id: RegistrarAction["id"]; - registrationLifecycle: RegistrarAction["registrationLifecycle"]; - registrant: RegistrarAction["registrant"]; - block: RegistrarAction["block"]; - transactionHash: RegistrarAction["transactionHash"]; - eventIds: RegistrarAction["eventIds"]; - }, - { expiresAt }: { expiresAt: UnixTimestamp }, -) { - const { node, subregistry } = registrationLifecycle; - const { subregistryId } = subregistry; - const type = RegistrarActionTypes.Renewal; - - // 1. Create logical event key - const logicalEventKey = makeLogicalEventKey({ - node, - subregistryId, - transactionHash, - }); - - // 2. Store mapping between logical event key and logical event id - await context.db.insert(schema.tempLogicalSubregistryAction).values({ - logicalEventKey, - logicalEventId: id, - }); - - // 3. Calculate incremental duration - const currentRegistrationLifecycle = await getRegistrationLifecycle(context, { - node, - }); - - if (!currentRegistrationLifecycle) { - throw new Error( - `Current Registration Lifecycle record was not found for node '${registrationLifecycle.node}'`, - ); - } - - const incrementalDuration = deserializeDuration( - expiresAt - bigIntToNumber(currentRegistrationLifecycle.expiresAt), - ); - - // 4. Store initial record for the "logical" Registrar Action - await context.db.insert(schema.registrarAction).values({ - id, - subregistryId: serializeAccountId(subregistryId), - type, - node, - incrementalDuration: BigInt(incrementalDuration), - registrant, - blockNumber: BigInt(block.number), - blockTimestamp: BigInt(block.timestamp), - transactionHash, - eventIds, - }); -} - -/** - * Handle registration event - */ -export async function handleRegistration( - context: Context, - event: Event, - { - subregistryId, - node, - expiresAt, - registrant, }: { + id: Event["id"]; subregistryId: AccountId; node: Node; - expiresAt: UnixTimestamp; registrant: Address; + expiresAt: UnixTimestamp; + block: BlockRef; + transactionHash: Hash; }, ) { // 0. Handle possible subsequent registration. @@ -280,7 +67,7 @@ export async function handleRegistration( }); } - // 2. Initialize the "logical" Registrar Action record for Registration + // 1. Get subregistry details. const subregistry = await getSubregistry(context, { subregistryId }); // Invariant: subregistry record must exist @@ -288,30 +75,30 @@ export async function handleRegistration( throw new Error(`Subregistry record must exists for '${serializeAccountId(subregistryId)}.'`); } - await initializeRegistrarActionRegistration( - context, - { - id: event.id, - registrationLifecycle: { - expiresAt, - node, - subregistry: { - subregistryId, - node: subregistry.node, - }, - }, - registrant, - block: { - number: bigIntToNumber(event.block.number), - timestamp: bigIntToNumber(event.block.timestamp), - }, - transactionHash: event.transaction.hash, - eventIds: [event.id], - }, - { + // 3. Calculate incremental duration + const incrementalDuration = durationBetween( + block.timestamp, // current block timestamp + expiresAt, // registrations lifecycle expiry date + ); + + // 4. Initialize the "logical" Registrar Action record for Registration + await initializeRegistrarAction(context, { + id, + type: RegistrarActionTypes.Registration, + registrationLifecycle: { expiresAt, + node, + subregistry: { + subregistryId, + node: subregistry.node, + }, }, - ); + incrementalDuration, + registrant, + block, + transactionHash, + eventIds: [id], + }); } /** @@ -319,17 +106,22 @@ export async function handleRegistration( */ export async function handleRenewal( context: Context, - event: Event, { + id, subregistryId, node, - expiresAt, registrant, + expiresAt, + block, + transactionHash, }: { + id: Event["id"]; subregistryId: AccountId; node: Node; - expiresAt: UnixTimestamp; registrant: Address; + expiresAt: UnixTimestamp; + block: BlockRef; + transactionHash: Hash; }, ) { // TODO: 0. enforce an invariant that for Renewal actions, @@ -337,7 +129,7 @@ export async function handleRenewal( // We can't add the state invariant about name renewals yet, because // doing so would require us to index more historical RegistrarControllers - // 1. Initialize the "logical" Registrar Action record for Renewal + // 1. Get subregistry details. const subregistry = await getSubregistry(context, { subregistryId }); // Invariant: subregistry record must exist @@ -345,34 +137,41 @@ export async function handleRenewal( throw new Error(`Subregistry record must exists for '${serializeAccountId(subregistryId)}.'`); } - await initializeRegistrarActionRenewal( - context, - { - id: event.id, - registrationLifecycle: { - expiresAt, - node, - subregistry: { - subregistryId, - node: subregistry.node, - }, - }, - registrant, - block: { - number: bigIntToNumber(event.block.number), - timestamp: bigIntToNumber(event.block.timestamp), - }, - transactionHash: event.transaction.hash, - eventIds: [event.id], - }, - { - expiresAt, - }, + // 2. Get the current registration lifecycle before this registrar action + // could update it. + const currentRegistrationLifecycle = await getRegistrationLifecycle(context, { + node, + }); + + if (!currentRegistrationLifecycle) { + throw new Error(`Current Registration Lifecycle record was not found for node '${node}'`); + } + + // 3. Calculate incremental duration + const incrementalDuration = durationBetween( + bigIntToNumber(currentRegistrationLifecycle.expiresAt), // current expiry date + expiresAt, // new expiry date ); - // 2. Extend Registration's expiry after creating the Registrar Action - // record. This is important for calculating incremental duration - // value correctly. + // 4. Initialize the "logical" Registrar Action record for Renewal + await initializeRegistrarAction(context, { + id, + type: RegistrarActionTypes.Renewal, + registrationLifecycle: { + expiresAt, + node, + subregistry: { + subregistryId, + node: subregistry.node, + }, + }, + incrementalDuration, + registrant, + block, + transactionHash, + eventIds: [id], + }); - await extendRegistration(context, { node, expiresAt }); + // 5. Extend Registration Lifecycle's expiry. + await extendRegistrationLifecycle(context, { node, expiresAt }); } diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registration-lifecycle.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registration-lifecycle.ts new file mode 100644 index 0000000000..2ebb4e9f92 --- /dev/null +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registration-lifecycle.ts @@ -0,0 +1,88 @@ +import type { Context } from "ponder:registry"; +import schema from "ponder:schema"; + +import { + type AccountId, + type Node, + serializeAccountId, + type UnixTimestamp, +} from "@ensnode/ensnode-sdk"; + +/** + * Get RegistrationLifecycle by node value. + */ +export async function getRegistrationLifecycle(context: Context, { node }: { node: Node }) { + return context.db.find(schema.registrationLifecycles, { node }); +} + +/** + * Make first registration + * + * Inserts a new record to track the current state of + * the Registration Lifecycle by node value. + */ +export async function makeFirstRegistration( + context: Context, + { + subregistryId, + node, + expiresAt, + }: { + subregistryId: AccountId; + node: Node; + expiresAt: UnixTimestamp; + }, +) { + return context.db.insert(schema.registrationLifecycles).values({ + subregistryId: serializeAccountId(subregistryId), + node, + expiresAt: BigInt(expiresAt), + }); +} + +/** + * Make subsequent registration + * + * Updates the current state of the Registration Lifecycle by node value. + * + * Note: this is a simplified approach where we override the expiry date of + * the registration lifecycle record for the node value. + * We took the simplified option to cut the scope. However, the ideal approach + * would create another Registration Lifecycle record for the subsequent + * registration, as it means the previous registration for the node went + * through all possible {@link RegistrationLifecycleStages}. + */ +export async function makeSubsequentRegistration( + context: Context, + { + node, + expiresAt, + }: { + node: Node; + expiresAt: UnixTimestamp; + }, +) { + return context.db + .update(schema.registrationLifecycles, { node }) + .set({ expiresAt: BigInt(expiresAt) }); +} + +/** + * Extend Registration Lifecycle + * + * Updates the current state of the Registration Lifecycle by node value. + */ +export async function extendRegistrationLifecycle( + context: Context, + { + node, + expiresAt, + }: { + node: Node; + expiresAt: UnixTimestamp; + }, +) { + return context.db + .update(schema.registrationLifecycles, { node }) + .set({ expiresAt: BigInt(expiresAt) }); +} diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts index 81493afda2..51c29f1918 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts @@ -7,6 +7,11 @@ import schema from "ponder:schema"; import { type AccountId, type Node, serializeAccountId } from "@ensnode/ensnode-sdk"; +/** + * Upsert Subregistry record + * + * If the record already exists, do noting. + */ export async function upsertSubregistry( context: Context, { @@ -18,10 +23,22 @@ export async function upsertSubregistry( }, ) { await context.db - .insert(schema.subregistry) + .insert(schema.subregistries) .values({ subregistryId: serializeAccountId(subregistryId), node, }) .onConflictDoNothing(); } + +/** + * Get Subregistry record by AccountId. + */ +export async function getSubregistry( + context: Context, + { subregistryId }: { subregistryId: AccountId }, +) { + return context.db.find(schema.subregistries, { + subregistryId: serializeAccountId(subregistryId), + }); +} From 24af62ab8d1e74406b11ef710632c56d530268dd Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 6 Nov 2025 09:26:57 +0100 Subject: [PATCH 09/13] fix(ensnode-schema): drop unused index --- packages/ensnode-schema/src/schemas/registrars.schema.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ensnode-schema/src/schemas/registrars.schema.ts b/packages/ensnode-schema/src/schemas/registrars.schema.ts index 67baf9080e..31a0ce0546 100644 --- a/packages/ensnode-schema/src/schemas/registrars.schema.ts +++ b/packages/ensnode-schema/src/schemas/registrars.schema.ts @@ -378,7 +378,6 @@ export const registrarActions = onchainTable( eventIds: t.text().array().notNull(), }), (t) => ({ - byRegistrant: index().on(t.registrant), byDecodedReferrer: index().on(t.decodedReferrer), byTimestamp: index().on(t.timestamp), }), From 84df66d30ce88ccf7ed23647875f175066294a7d Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 6 Nov 2025 10:39:03 +0100 Subject: [PATCH 10/13] refactor(ensindexer): rename event handlers for `registrars` plugin --- .../basenames/handlers/Basenames_Registrar.ts | 11 +++-- .../ethnames/handlers/Ethnames_Registrar.ts | 9 ++-- .../handlers/Lineanames_Registrar.ts | 9 ++-- .../registrars/shared/lib/registrar-action.ts | 9 ++-- .../registrars/shared/lib/registrar-events.ts | 8 ++-- .../src/schemas/registrars.schema.ts | 41 ++++++++++--------- 6 files changed, 51 insertions(+), 36 deletions(-) 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 77af19ccf3..fdf2b3f9c6 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts @@ -9,7 +9,10 @@ import { type BlockRef, bigIntToNumber, makeSubdomainNode, PluginName } from "@e import { getDatasourceContract } from "@/lib/datasource-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; -import { handleRegistration, handleRenewal } from "../../shared/lib/registrar-events"; +import { + handleRegistrarEventRegistration, + handleRegistrarEventRenewal, +} from "../../shared/lib/registrar-events"; import { upsertSubregistry } from "../../shared/lib/subregistry"; import { getRegistrarManagedName, tokenIdToLabelHash } from "../lib/registrar-helpers"; @@ -47,7 +50,7 @@ export default function () { await upsertSubregistry(context, subregistry); - await handleRegistration(context, { + await handleRegistrarEventRegistration(context, { id, subregistryId, node, @@ -75,7 +78,7 @@ export default function () { await upsertSubregistry(context, subregistry); - await handleRegistration(context, { + await handleRegistrarEventRegistration(context, { id, subregistryId, node, @@ -101,7 +104,7 @@ export default function () { } satisfies BlockRef; const transactionHash = event.transaction.hash; - await handleRenewal(context, { + await handleRegistrarEventRenewal(context, { id, subregistryId, node, 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 1d3691e103..5b70d084f8 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts @@ -9,7 +9,10 @@ import { type BlockRef, bigIntToNumber, makeSubdomainNode, PluginName } from "@e import { getDatasourceContract } from "@/lib/datasource-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; -import { handleRegistration, handleRenewal } from "../../shared/lib/registrar-events"; +import { + handleRegistrarEventRegistration, + handleRegistrarEventRenewal, +} from "../../shared/lib/registrar-events"; import { upsertSubregistry } from "../../shared/lib/subregistry"; import { getRegistrarManagedName, tokenIdToLabelHash } from "../lib/registrar-helpers"; @@ -46,7 +49,7 @@ export default function () { await upsertSubregistry(context, subregistry); - await handleRegistration(context, { + await handleRegistrarEventRegistration(context, { id, subregistryId, node, @@ -72,7 +75,7 @@ export default function () { } satisfies BlockRef; const transactionHash = event.transaction.hash; - await handleRenewal(context, { + await handleRegistrarEventRenewal(context, { id, subregistryId, node, 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 e49c050706..d6ed1e4268 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts @@ -9,7 +9,10 @@ import { type BlockRef, bigIntToNumber, makeSubdomainNode, PluginName } from "@e import { getDatasourceContract } from "@/lib/datasource-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; -import { handleRegistration, handleRenewal } from "../../shared/lib/registrar-events"; +import { + handleRegistrarEventRegistration, + handleRegistrarEventRenewal, +} from "../../shared/lib/registrar-events"; import { upsertSubregistry } from "../../shared/lib/subregistry"; import { getRegistrarManagedName, tokenIdToLabelHash } from "../lib/registrar-helpers"; @@ -46,7 +49,7 @@ export default function () { await upsertSubregistry(context, subregistry); - await handleRegistration(context, { + await handleRegistrarEventRegistration(context, { id, subregistryId, node, @@ -72,7 +75,7 @@ export default function () { } satisfies BlockRef; const transactionHash = event.transaction.hash; - await handleRenewal(context, { + await handleRegistrarEventRenewal(context, { id, subregistryId, node, diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts index 7776713f09..01c0accc45 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts @@ -44,7 +44,7 @@ export async function getLogicalRegistrarActionByEventKey( context: Context, logicalEventKey: LogicalEventKey, ) { - const tempRecord = await context.db.find(schema.tempLogicalSubregistryAction, { + const tempRecord = await context.db.find(schema.internal_subregistryActionMetadata, { logicalEventKey, }); @@ -68,6 +68,9 @@ export async function getLogicalRegistrarActionByEventKey( ); } + // Drop the temp record, as it won't be needed anymore. + await context.db.delete(schema.internal_subregistryActionMetadata, { logicalEventKey }); + return logicalRegistrarAction; } @@ -98,7 +101,7 @@ export async function initializeRegistrarAction( }); // 2. Store mapping between logical event key and logical event id - await context.db.insert(schema.tempLogicalSubregistryAction).values({ + await context.db.insert(schema.internal_subregistryActionMetadata).values({ logicalEventKey, logicalEventId: id, }); @@ -106,8 +109,8 @@ export async function initializeRegistrarAction( // 4. Store initial record for the "logical" Registrar Action await context.db.insert(schema.registrarActions).values({ id, - subregistryId: serializeAccountId(subregistryId), type, + subregistryId: serializeAccountId(subregistryId), node, incrementalDuration: BigInt(incrementalDuration), registrant, diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts index d6bbf86759..5d8062992f 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts @@ -26,9 +26,9 @@ import { import { getSubregistry } from "./subregistry"; /** - * Handle registration event + * Handle Registrar Event: Registration */ -export async function handleRegistration( +export async function handleRegistrarEventRegistration( context: Context, { id, @@ -102,9 +102,9 @@ export async function handleRegistration( } /** - * Handle Renewal + * Handle Registrar Event: Renewal */ -export async function handleRenewal( +export async function handleRegistrarEventRenewal( context: Context, { id, diff --git a/packages/ensnode-schema/src/schemas/registrars.schema.ts b/packages/ensnode-schema/src/schemas/registrars.schema.ts index 31a0ce0546..7c05fcea04 100644 --- a/packages/ensnode-schema/src/schemas/registrars.schema.ts +++ b/packages/ensnode-schema/src/schemas/registrars.schema.ts @@ -402,26 +402,29 @@ export const registrarActions = onchainTable( * the last event handler for a "logical subregistry action" has completed its * processing the record referenced by the `logicalEventKey` must be removed. */ -export const tempLogicalSubregistryAction = onchainTable("_subregistry_action_metadata", (t) => ({ - /** - * Logical Event Key - * - * A string formatted as: - * `{chainId}:{subregistryAddress}:{node}:{transactionHash}` - */ - logicalEventKey: t.text().primaryKey(), +export const internal_subregistryActionMetadata = onchainTable( + "_ensindexer_subregistry_action_metadata", + (t) => ({ + /** + * Logical Event Key + * + * A string formatted as: + * `{chainId}:{subregistryAddress}:{node}:{transactionHash}` + */ + logicalEventKey: t.text().primaryKey(), - /** - * Logical Event ID - * - * A string holding the `id` value of the existing "logical registrar action" - * record that is currently being built as an aggregation of onchain events. - * - * May be used by subsequent event handlers to identify which - * "logical registrar action" to aggregate additional indexed state into. - */ - logicalEventId: t.text().notNull(), -})); + /** + * Logical Event ID + * + * A string holding the `id` value of the existing "logical registrar action" + * record that is currently being built as an aggregation of onchain events. + * + * May be used by subsequent event handlers to identify which + * "logical registrar action" to aggregate additional indexed state into. + */ + logicalEventId: t.text().notNull(), + }), +); /// Relations From 8b27c167301ddc475526c01d5668375059ae892e Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 6 Nov 2025 18:33:26 +0100 Subject: [PATCH 11/13] apply pr feedback --- .../src/plugins/registrars/README.md | 1 - .../basenames/handlers/Basenames_Registrar.ts | 10 +- .../ethnames/handlers/Ethnames_Registrar.ts | 10 +- .../handlers/Lineanames_Registrar.ts | 10 +- .../Lineanames_RegistrarController.ts | 55 +++-- .../registrars/shared/lib/registrar-action.ts | 55 +---- .../shared/lib/registrar-controller-events.ts | 41 +++- .../registrars/shared/lib/registrar-events.ts | 25 ++- .../shared/lib/registration-lifecycle.ts | 48 ++--- .../registrars/shared/lib/subregistry.ts | 8 +- .../src/schemas/registrars.schema.ts | 110 ++++++---- .../src/registrars/registrar-action.ts | 200 +++++++++++------- .../src/registrars/registration-lifecycle.ts | 22 +- .../ensnode-sdk/src/registrars/subregistry.ts | 8 +- .../ensnode-sdk/src/shared/datetime.test.ts | 3 +- .../ensnode-sdk/src/shared/numbers.test.ts | 28 +-- packages/ensnode-sdk/src/shared/numbers.ts | 4 +- 17 files changed, 367 insertions(+), 271 deletions(-) diff --git a/apps/ensindexer/src/plugins/registrars/README.md b/apps/ensindexer/src/plugins/registrars/README.md index 4607fcbd3f..6f2f8f60ba 100644 --- a/apps/ensindexer/src/plugins/registrars/README.md +++ b/apps/ensindexer/src/plugins/registrars/README.md @@ -6,5 +6,4 @@ This plugin enables tracking all registrations and renewals that ever happened f - direct subnames of the Lineanames registrar managed name (ex: for mainnet `linea.eth` but varies for other namespaces). Additionally indexes: -- All Registrar Controllers ever associated with a known Registrar contract. - All ENS Referrals (for Registrar Controllers supporting ENS Referral Programs). 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 fdf2b3f9c6..74e6ec3acb 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_Registrar.ts @@ -4,7 +4,13 @@ import { ponder } from "ponder:registry"; import { namehash } from "viem/ens"; import { DatasourceNames } from "@ensnode/datasources"; -import { type BlockRef, bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; +import { + type BlockRef, + bigIntToNumber, + makeSubdomainNode, + PluginName, + type Subregistry, +} from "@ensnode/ensnode-sdk"; import { getDatasourceContract } from "@/lib/datasource-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; @@ -31,7 +37,7 @@ export default function () { const subregistry = { subregistryId, node: parentNode, - }; + } satisfies Subregistry; // support NameRegisteredWithRecord for BaseRegistrar as it used by Base's RegistrarControllers ponder.on( 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 5b70d084f8..808690960b 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_Registrar.ts @@ -4,7 +4,13 @@ import { ponder } from "ponder:registry"; import { namehash } from "viem/ens"; import { DatasourceNames } from "@ensnode/datasources"; -import { type BlockRef, bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; +import { + type BlockRef, + bigIntToNumber, + makeSubdomainNode, + PluginName, + type Subregistry, +} from "@ensnode/ensnode-sdk"; import { getDatasourceContract } from "@/lib/datasource-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; @@ -31,7 +37,7 @@ export default function () { const subregistry = { subregistryId, node: parentNode, - }; + } satisfies Subregistry; ponder.on( namespaceContract(pluginName, "Ethnames_BaseRegistrar:NameRegistered"), 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 d6ed1e4268..75044a8b07 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_Registrar.ts @@ -4,7 +4,13 @@ import { ponder } from "ponder:registry"; import { namehash } from "viem/ens"; import { DatasourceNames } from "@ensnode/datasources"; -import { type BlockRef, bigIntToNumber, makeSubdomainNode, PluginName } from "@ensnode/ensnode-sdk"; +import { + type BlockRef, + bigIntToNumber, + makeSubdomainNode, + PluginName, + type Subregistry, +} from "@ensnode/ensnode-sdk"; import { getDatasourceContract } from "@/lib/datasource-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; @@ -31,7 +37,7 @@ export default function () { const subregistry = { subregistryId, node: parentNode, - }; + } satisfies Subregistry; ponder.on( namespaceContract(pluginName, "Lineanames_BaseRegistrar:NameRegistered"), 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 e409bbb310..1ea18bb8cb 100644 --- a/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/lineanames/handlers/Lineanames_RegistrarController.ts @@ -5,9 +5,11 @@ import { namehash } from "viem/ens"; import { DatasourceNames } from "@ensnode/datasources"; import { + addPrices, makeSubdomainNode, PluginName, - type RegistrarActionPricingNotApplicable, + priceEth, + type RegistrarActionPricingAvailable, type RegistrarActionReferralNotApplicable, } from "@ensnode/ensnode-sdk"; @@ -30,17 +32,6 @@ export default function () { "BaseRegistrar", ); - /** - * No Registrar Controller for Lineanames implements premiums or - * emits distinct baseCost or premium (as opposed to just a simple price) - * in events. - */ - const pricing = { - baseCost: null, - premium: null, - total: null, - } satisfies RegistrarActionPricingNotApplicable; - /** * No Registrar Controller for Lineanames implements referrals or * emits a referrer in events. @@ -62,6 +53,17 @@ export default function () { const node = makeSubdomainNode(labelHash, parentNode); const transactionHash = event.transaction.hash; + /** + * The `OwnerNameRegistered` event emitted by + * `Lineanames_EthRegistrarController` contract is akin to + * the `NameRegistered` event with `baseCost` of `0` and `premium` of `0`. + */ + const pricing = { + baseCost: priceEth(0n), + premium: priceEth(0n), + total: priceEth(0n), + } satisfies RegistrarActionPricingAvailable; + await handleRegistrarControllerEvent(context, { id, subregistryId, @@ -81,6 +83,17 @@ export default function () { const node = makeSubdomainNode(labelHash, parentNode); const transactionHash = event.transaction.hash; + /** + * The `PohNameRegistered` event emitted by + * `Lineanames_EthRegistrarController` contract is akin to + * the `NameRegistered` event with `baseCost` of `0` and `premium` of `0`. + */ + const pricing = { + baseCost: priceEth(0n), + premium: priceEth(0n), + total: priceEth(0n), + } satisfies RegistrarActionPricingAvailable; + await handleRegistrarControllerEvent(context, { id, subregistryId, @@ -100,6 +113,15 @@ export default function () { const node = makeSubdomainNode(labelHash, parentNode); const transactionHash = event.transaction.hash; + const baseCost = priceEth(event.args.baseCost); + const premium = priceEth(event.args.premium); + const total = addPrices(baseCost, premium); + const pricing = { + baseCost, + premium, + total, + } satisfies RegistrarActionPricingAvailable; + await handleRegistrarControllerEvent(context, { id, subregistryId, @@ -119,6 +141,15 @@ export default function () { const node = makeSubdomainNode(labelHash, parentNode); const transactionHash = event.transaction.hash; + const baseCost = priceEth(event.args.cost); + const premium = priceEth(0n); // premium for renewals is always 0 + const total = baseCost; + const pricing = { + baseCost, + premium, + total, + } satisfies RegistrarActionPricingAvailable; + await handleRegistrarControllerEvent(context, { id, subregistryId, diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts index 01c0accc45..b124fc202b 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-action.ts @@ -12,7 +12,7 @@ import { /** * Logical Event Key * - * String formatted as: + * Fully lowercase string formatted as: * `{accountId}:{node}:{transactionHash}`, where `accountId` follows * the CAIP-10 standard. * @@ -21,7 +21,7 @@ import { export type LogicalEventKey = string; /** - * Make a logical event key for a "logical" registrar action. + * Make a logical event key for a "logical registrar action". */ export function makeLogicalEventKey({ subregistryId, @@ -32,52 +32,13 @@ export function makeLogicalEventKey({ node: Node; transactionHash: Hash; }): LogicalEventKey { - return [serializeAccountId(subregistryId), node, transactionHash].join(":"); + return [serializeAccountId(subregistryId), node, transactionHash].join(":").toLowerCase(); } /** - * Get "logical" Registrar Action record by logical event key. - * - * @throws if the record cannot be found. - */ -export async function getLogicalRegistrarActionByEventKey( - context: Context, - logicalEventKey: LogicalEventKey, -) { - const tempRecord = await context.db.find(schema.internal_subregistryActionMetadata, { - logicalEventKey, - }); - - // Invariant: the "logical" Registrar Action ID must be available - if (!tempRecord) { - throw new Error( - `The required "logical" Registrar Action ID could not be found for the following logical event key: '${logicalEventKey}'.`, - ); - } - - const { logicalEventId } = tempRecord; - - const logicalRegistrarAction = await context.db.find(schema.registrarActions, { - id: logicalEventId, - }); - - // Invariant: the "logical" Registrar Action record must be available - if (!logicalRegistrarAction) { - throw new Error( - `The "logical" Registrar Action record, which could not be found for the following logical event ID: '${logicalEventId}'.`, - ); - } - - // Drop the temp record, as it won't be needed anymore. - await context.db.delete(schema.internal_subregistryActionMetadata, { logicalEventKey }); - - return logicalRegistrarAction; -} - -/** - * Initialize a record for the "logical" Registrar Action. + * Insert a record for the "logical registrar action". */ -export async function initializeRegistrarAction( +export async function insertRegistrarAction( context: Context, { id, @@ -89,7 +50,7 @@ export async function initializeRegistrarAction( transactionHash, eventIds, }: Omit, -) { +): Promise { const { node, subregistry } = registrationLifecycle; const { subregistryId } = subregistry; @@ -101,12 +62,12 @@ export async function initializeRegistrarAction( }); // 2. Store mapping between logical event key and logical event id - await context.db.insert(schema.internal_subregistryActionMetadata).values({ + await context.db.insert(schema.internal_registrarActionMetadata).values({ logicalEventKey, logicalEventId: id, }); - // 4. Store initial record for the "logical" Registrar Action + // 3. Store initial record for the "logical registrar action" await context.db.insert(schema.registrarActions).values({ id, type, diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts index 3664c6db83..ca55d50132 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts @@ -12,10 +12,10 @@ import { type RegistrarActionReferral, } from "@ensnode/ensnode-sdk"; -import { getLogicalRegistrarActionByEventKey, makeLogicalEventKey } from "./registrar-action"; +import { makeLogicalEventKey } from "./registrar-action"; /** - * Update the "logical" Registrar Action: + * Update the "logical registrar action": * - set pricing data (if available) * - set referral data (if available) * - append new event ID to `eventIds` @@ -37,7 +37,7 @@ export async function handleRegistrarControllerEvent( referral: RegistrarActionReferral; transactionHash: Hash; }, -) { +): Promise { // 1. Make Logical Event Key const logicalEventKey = makeLogicalEventKey({ subregistryId, @@ -45,12 +45,37 @@ export async function handleRegistrarControllerEvent( transactionHash, }); - // 2. Use the Logical Event Key to get the "logical" Registrar Action record + // 2. Use the Logical Event Key to get the "logical registrar action" record // which needs to be updated. - const logicalRegistrarAction = await getLogicalRegistrarActionByEventKey( - context, + + // 2. a) Find subregistryActionMetadata record by logical event key. + const subregistryActionMetadata = await context.db.find(schema.internal_registrarActionMetadata, { logicalEventKey, - ); + }); + + // Invariant: the subregistryActionMetadata record must be available for `logicalEventKey` + if (!subregistryActionMetadata) { + throw new Error( + `The required "logical registrar action" ID could not be found for the following logical event key: '${logicalEventKey}'.`, + ); + } + + const { logicalEventId } = subregistryActionMetadata; + + // 2. b) Find "logical registrar action" record by `logicalEventId`. + const logicalRegistrarAction = await context.db.find(schema.registrarActions, { + id: logicalEventId, + }); + + // Invariant: the "logical registrar action" record must be available for `logicalEventId` + if (!logicalRegistrarAction) { + throw new Error( + `The "logical registrar action" record, which could not be found for the following logical event ID: '${logicalEventId}'.`, + ); + } + + // 2. c) Drop the subregistryActionMetadata record, as it won't be needed anymore. + await context.db.delete(schema.internal_registrarActionMetadata, { logicalEventKey }); // 3. Prepare pricing info let baseCost: bigint | null; @@ -79,7 +104,7 @@ export async function handleRegistrarControllerEvent( decodedReferrer = null; } - // 5. Update the "logical" Registrar Action record with + // 5. Update the "logical registrar action" record with // - pricing data, // - referral data // - new event ID appended to `eventIds` diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts index 5d8062992f..cb7985a686 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registrar-events.ts @@ -16,12 +16,11 @@ import { type UnixTimestamp, } from "@ensnode/ensnode-sdk"; -import { initializeRegistrarAction } from "./registrar-action"; +import { insertRegistrarAction } from "./registrar-action"; import { - extendRegistrationLifecycle, getRegistrationLifecycle, - makeFirstRegistration, - makeSubsequentRegistration, + insertRegistrationLifecycle, + updateRegistrationLifecycle, } from "./registration-lifecycle"; import { getSubregistry } from "./subregistry"; @@ -47,7 +46,7 @@ export async function handleRegistrarEventRegistration( block: BlockRef; transactionHash: Hash; }, -) { +): Promise { // 0. Handle possible subsequent registration. // Get the state of a possibly indexed registration record for this node // before this registration occurred. @@ -57,10 +56,10 @@ export async function handleRegistrarEventRegistration( // 1. If a RegistrationLifecycle for the `node` has been already indexed, // it means that another RegistrationLifecycle was made for the `node` after // the previously indexed RegistrationLifecycle expired and its grace period ended. - await makeSubsequentRegistration(context, { node, expiresAt }); + await updateRegistrationLifecycle(context, { node, expiresAt }); } else { // 1. It's a first-time registration made for the `node` value. - await makeFirstRegistration(context, { + await insertRegistrationLifecycle(context, { subregistryId, node, expiresAt, @@ -81,8 +80,8 @@ export async function handleRegistrarEventRegistration( expiresAt, // registrations lifecycle expiry date ); - // 4. Initialize the "logical" Registrar Action record for Registration - await initializeRegistrarAction(context, { + // 4. Initialize the "logical registrar action" record for Registration + await insertRegistrarAction(context, { id, type: RegistrarActionTypes.Registration, registrationLifecycle: { @@ -123,7 +122,7 @@ export async function handleRegistrarEventRenewal( block: BlockRef; transactionHash: Hash; }, -) { +): Promise { // TODO: 0. enforce an invariant that for Renewal actions, // the registration must be in a "renewable" state. // We can't add the state invariant about name renewals yet, because @@ -153,8 +152,8 @@ export async function handleRegistrarEventRenewal( expiresAt, // new expiry date ); - // 4. Initialize the "logical" Registrar Action record for Renewal - await initializeRegistrarAction(context, { + // 4. Initialize the "logical registrar action" record for Renewal + await insertRegistrarAction(context, { id, type: RegistrarActionTypes.Renewal, registrationLifecycle: { @@ -173,5 +172,5 @@ export async function handleRegistrarEventRenewal( }); // 5. Extend Registration Lifecycle's expiry. - await extendRegistrationLifecycle(context, { node, expiresAt }); + await updateRegistrationLifecycle(context, { node, expiresAt }); } diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/registration-lifecycle.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/registration-lifecycle.ts index 2ebb4e9f92..4cc4491ad5 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/registration-lifecycle.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/registration-lifecycle.ts @@ -11,17 +11,20 @@ import { /** * Get RegistrationLifecycle by node value. */ -export async function getRegistrationLifecycle(context: Context, { node }: { node: Node }) { +export async function getRegistrationLifecycle( + context: Context, + { node }: { node: Node }, +): Promise { return context.db.find(schema.registrationLifecycles, { node }); } /** - * Make first registration + * Insert Registration Lifecycle * * Inserts a new record to track the current state of * the Registration Lifecycle by node value. */ -export async function makeFirstRegistration( +export async function insertRegistrationLifecycle( context: Context, { subregistryId, @@ -32,8 +35,8 @@ export async function makeFirstRegistration( node: Node; expiresAt: UnixTimestamp; }, -) { - return context.db.insert(schema.registrationLifecycles).values({ +): Promise { + await context.db.insert(schema.registrationLifecycles).values({ subregistryId: serializeAccountId(subregistryId), node, expiresAt: BigInt(expiresAt), @@ -41,38 +44,11 @@ export async function makeFirstRegistration( } /** - * Make subsequent registration - * - * Updates the current state of the Registration Lifecycle by node value. - * - * Note: this is a simplified approach where we override the expiry date of - * the registration lifecycle record for the node value. - * We took the simplified option to cut the scope. However, the ideal approach - * would create another Registration Lifecycle record for the subsequent - * registration, as it means the previous registration for the node went - * through all possible {@link RegistrationLifecycleStages}. - */ -export async function makeSubsequentRegistration( - context: Context, - { - node, - expiresAt, - }: { - node: Node; - expiresAt: UnixTimestamp; - }, -) { - return context.db - .update(schema.registrationLifecycles, { node }) - .set({ expiresAt: BigInt(expiresAt) }); -} - -/** - * Extend Registration Lifecycle + * Upsert Registration Lifecycle * * Updates the current state of the Registration Lifecycle by node value. */ -export async function extendRegistrationLifecycle( +export async function updateRegistrationLifecycle( context: Context, { node, @@ -81,8 +57,8 @@ export async function extendRegistrationLifecycle( node: Node; expiresAt: UnixTimestamp; }, -) { - return context.db +): Promise { + await context.db .update(schema.registrationLifecycles, { node }) .set({ expiresAt: BigInt(expiresAt) }); } diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts index 51c29f1918..880e8d9a40 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/subregistry.ts @@ -1,5 +1,5 @@ /** - * This file contains handlers used in event handlers for a Registrar contract. + * This file contains handlers used in event handlers for a subregistry contract. */ import type { Context } from "ponder:registry"; @@ -10,7 +10,7 @@ import { type AccountId, type Node, serializeAccountId } from "@ensnode/ensnode- /** * Upsert Subregistry record * - * If the record already exists, do noting. + * If the record already exists, do nothing. */ export async function upsertSubregistry( context: Context, @@ -21,7 +21,7 @@ export async function upsertSubregistry( subregistryId: AccountId; node: Node; }, -) { +): Promise { await context.db .insert(schema.subregistries) .values({ @@ -37,7 +37,7 @@ export async function upsertSubregistry( export async function getSubregistry( context: Context, { subregistryId }: { subregistryId: AccountId }, -) { +): Promise { return context.db.find(schema.subregistries, { subregistryId: serializeAccountId(subregistryId), }); diff --git a/packages/ensnode-schema/src/schemas/registrars.schema.ts b/packages/ensnode-schema/src/schemas/registrars.schema.ts index 7c05fcea04..59712edb6d 100644 --- a/packages/ensnode-schema/src/schemas/registrars.schema.ts +++ b/packages/ensnode-schema/src/schemas/registrars.schema.ts @@ -1,5 +1,5 @@ /** - * Schema Definitions for tracking of ENS subregistries. + * Schema Definitions for tracking of ENS registrars. */ import { index, onchainEnum, onchainTable, relations, uniqueIndex } from "ponder"; @@ -18,7 +18,8 @@ export const subregistries = onchainTable( * Identifies the chainId and address of the smart contract associated * with the subregistry. * - * Guaranteed to be a string formatted according to the CAIP-10 standard. + * Guaranteed to be a fully lowercase string formatted according to + * the CAIP-10 standard. * * @see https://chainagnostic.org/CAIPs/caip-10 */ @@ -31,7 +32,7 @@ export const subregistries = onchainTable( * - `base.eth` * - `linea.eth` * - * Guaranteed to be a hex string representation of 32-bytes. + * Guaranteed to be a fully lowercase hex string representation of 32-bytes. */ node: t.hex().notNull(), }), @@ -77,7 +78,7 @@ export const registrationLifecycles = onchainTable( * Guaranteed to be a subname of the node (namehash) of the subregistry * identified by `subregistryId`. * - * Guaranteed to be a hex string representation of 32-bytes. + * Guaranteed to be a fully lowercase hex string representation of 32-bytes. */ node: t.hex().primaryKey(), @@ -87,7 +88,8 @@ export const registrationLifecycles = onchainTable( * Identifies the chainId and address of the subregistry smart contract * that manages the registration lifecycle. * - * Guaranteed to be a string formatted according to the CAIP-10 standard. + * Guaranteed to be a fully lowercase string formatted according to + * the CAIP-10 standard. * * @see https://chainagnostic.org/CAIPs/caip-10 */ @@ -106,9 +108,9 @@ export const registrationLifecycles = onchainTable( ); /** - * "Logical" Registrar Action Type Enum + * "Logical registrar action type" enum * - * Types of "logical" Registrar Actions. + * Types of "logical registrar action". */ export const registrarActionType = onchainEnum("registrar_action_type", [ "registration", @@ -116,7 +118,7 @@ export const registrarActionType = onchainEnum("registrar_action_type", [ ]); /** - * Logical Registrar Actions + * "Logical registrar actions" * * This table models "logical actions" rather than "events" because a single * "logical action", such as a single registration or renewal, may emit @@ -140,7 +142,7 @@ export const registrarActionType = onchainEnum("registrar_action_type", [ * - `incrementalDuration` * - `registrant` * 2. A "RegistrarController" contract emits its own `NameRegistered` event - * enabling the tracking of data including: + * enabling the tracking of data that may include: * - `baseCost` * - `premium` * - `total` @@ -153,7 +155,7 @@ export const registrarActions = onchainTable( "registrar_actions", (t) => ({ /** - * "Logical" Registrar Action ID + * "Logical registrar action" ID * * The `id` value is a deterministic and globally unique identifier for * the "logical registrar action". @@ -180,7 +182,8 @@ export const registrarActions = onchainTable( * Identifies the chainId and address of the associated subregistry smart * contract. * - * Guaranteed to be a string formatted according to the CAIP-10 standard. + * Guaranteed to be a fully lowercase string formatted according to + * the CAIP-10 standard. * * @see https://chainagnostic.org/CAIPs/caip-10 */ @@ -190,7 +193,7 @@ export const registrarActions = onchainTable( * The node (namehash) of the FQDN of the domain associated with * the "logical registrar action". * - * Guaranteed to be a hex string representation of 32-bytes. + * Guaranteed to be a fully lowercase hex string representation of 32-bytes. */ node: t.hex().notNull(), @@ -287,11 +290,20 @@ export const registrarActions = onchainTable( /** * Registrant * - * Refers to address on the same `chainId` as referred by `subregistryId` - * that initiated the "logical" Registrar Action and is paying - * the `total` cost. + * Identifies the address that initiated the "logical registrar action" and + * is paying the `total` cost (if applicable). * - * Guaranteed to be a string formatted according to the CAIP-10 standard. + * It may not be the owner of the name: + * 1. When a name is registered, the initial owner of the name may be + * distinct from the registrant. + * 2. There are no restrictions on who may renew a name. + * Therefore the owner of the name may be distinct from the registrant. + * + * + * The "chainId" of this address is the same as is referenced in `subregistryId`. + * + * Guaranteed to be a fully lowercase string formatted according to + * the CAIP-10 standard. */ registrant: t.text().notNull(), @@ -303,7 +315,7 @@ export const registrarActions = onchainTable( * * Guaranteed to be: * 1) null if the emitted `eventIds` contain no information about a referrer. - * 2) Otherwise, a hex string representation of 32-bytes. + * 2) Otherwise, a fully lowercase hex string representation of 32-bytes. */ encodedReferrer: t.hex(), @@ -313,34 +325,45 @@ export const registrarActions = onchainTable( * Decoded referrer according to the subjective interpretation of * `encodedReferrer` defined for ENS Holiday Awards. * - * Refers to address on the same `chainId` as referred by `subregistryId`. + * Identifies the interpreted address of the referrer. + * The "chainId" of this address is the same as is referenced in + * `subregistryId`. * * Guaranteed to be: * 1) null if `encodedReferrer` is null. - * 2) Otherwise, a valid EVM address (including zero address). + * 2) Otherwise, a fully lowercase address. + * 3) May be the "zero address" to represent that an `encodedReferrer` is + * defined but that it is interpreted as no referrer. */ decodedReferrer: t.hex(), /** - * Number of the block that includes the "logical" Registrar Action. + * Number of the block that includes the "logical registrar action". + * + * The "chainId" of this block is the same as is referenced in + * `subregistryId`. * * Guaranteed to be a non-negative bigint value. */ blockNumber: t.bigint().notNull(), /** - * Timestamp of the block that includes the "logical" Registrar Action. + * Unix timestamp of the block referenced by `blockNumber` that includes + * the "logical registrar action". */ timestamp: t.bigint().notNull(), /** - * Transaction hash of the transaction on `chainId` chain associated with - * the Logical Registrar Action. + * Transaction hash of the transaction associated with + * the "logical registrar action". + * + * The "chainId" of this transaction is the same as is referenced in + * `subregistryId`. * * Note that a single transaction may be associated with any number of - * "Logical" Registrar Actions. + * "logical registrar actions". * - * Guaranteed to be a string representation of 32-bytes. + * Guaranteed to be a fully lowercase hex string representation of 32-bytes. */ transactionHash: t.hex().notNull(), @@ -348,19 +371,18 @@ export const registrarActions = onchainTable( * Event IDs * * Array of the eventIds that have contributed to the state of - * the Logical Registrar Action. + * the "logical registrar action" record. * * Each eventId is a deterministic and globally unique onchain event * identifier. * * Guarantees: - * - Each eventId is of events that occurred on `chainId` within - * `blockNumber`. + * - Each eventId is of events that occurred within the block + * referenced by `blockNumber`. * - At least 1 eventId. - * - Ordered chronologically (ascending) by logIndex within `blockNumber` - * on `chainId`. + * - Ordered chronologically (ascending) by logIndex within `blockNumber`. * - The first element in the array is equal to the `id` of - * the "logical registrar action". + * the overall "logical registrar action" record. * * The following ideas are not generalized for ENS overall but happen to * be a characteristic of the scope of our current indexing logic: @@ -370,10 +392,6 @@ export const registrarActions = onchainTable( * a related "Registrar Controller" contract. This is because our * current indexing logic doesn't guarantee to index * all "Registrar Controller" contracts. - * - * Guaranteed to: - * - Reference at least one event. - * - Keep event references ordered chronologically, by event log index. */ eventIds: t.text().array().notNull(), }), @@ -384,31 +402,31 @@ export const registrarActions = onchainTable( ); /** - * Logical Subregistry Action Metadata + * Logical Registrar Action Metadata * * NOTE: This table is an internal implementation detail of ENSIndexer and * should not be queried outside of ENSIndexer. * - * Building a "logical subregistry action" record may require data from + * Building a "logical registrar action" record may require data from * multiple onchain events. To help aggregate data from multiple events into - * a single "logical subregistry action" ENSIndexer may temporarily store data + * a single "logical registrar action" ENSIndexer may temporarily store data * here to achieve this data aggregation. * - * Note how multiple "logical subregistry actions" may be taken on + * Note how multiple "logical registrar actions" may be taken on * the same `node` in the same `transactionHash`. For example, consider * a case of a single transaction registering a name and subsequently renewing * it twice. While this may be silly it is technically possible and therefore * such cases must be considered. To support such cases, when - * the last event handler for a "logical subregistry action" has completed its + * the last event handler for a "logical registrar action" has completed its * processing the record referenced by the `logicalEventKey` must be removed. */ -export const internal_subregistryActionMetadata = onchainTable( - "_ensindexer_subregistry_action_metadata", +export const internal_registrarActionMetadata = onchainTable( + "_ensindexer_registrar_action_metadata", (t) => ({ /** * Logical Event Key * - * A string formatted as: + * A fully lowercase string formatted as: * `{chainId}:{subregistryAddress}:{node}:{transactionHash}` */ logicalEventKey: t.text().primaryKey(), @@ -443,7 +461,7 @@ export const subregistryRelations = relations(subregistries, ({ many }) => ({ * * Each Registration Lifecycle is related to: * - exactly one Subregistry - * - 0 or more "logical" RegistrarActions + * - 0 or more "logical registrar action" */ export const registrationLifecycleRelations = relations( registrationLifecycles, @@ -458,9 +476,9 @@ export const registrationLifecycleRelations = relations( ); /** - * "Logical" Registrar Action Relations + * "Logical registrar action" Relations * - * Each "logical" Registrar Action is related to: + * Each "logical registrar action" is related to: * - exactly one Registration Lifecycle (note the docs on * Registration Lifecycle explaining how these records may * be recycled across time). diff --git a/packages/ensnode-sdk/src/registrars/registrar-action.ts b/packages/ensnode-sdk/src/registrars/registrar-action.ts index a60cd3ca9a..d0f3148a3c 100644 --- a/packages/ensnode-sdk/src/registrars/registrar-action.ts +++ b/packages/ensnode-sdk/src/registrars/registrar-action.ts @@ -14,7 +14,7 @@ import type { RegistrationLifecycle } from "./registration-lifecycle"; type RegistrarActionEventId = string; /** - * Types of "logical" Registrar Action. + * Types of "logical registrar action". */ export const RegistrarActionTypes = { Registration: "registration", @@ -24,55 +24,66 @@ export const RegistrarActionTypes = { export type RegistrarActionType = (typeof RegistrarActionTypes)[keyof typeof RegistrarActionTypes]; /** - * Prices information for performing the "logical" registrar action. + * Pricing information for a "logical registrar action". */ export interface RegistrarActionPricingAvailable { /** * Base cost * - * Note: the "baseCost.amount" may be`0` or more. + * Base cost (before any `premium`) of Ether measured in units of Wei + * paid to execute the "logical registrar action". + * + * May be 0. */ baseCost: PriceEth; /** * Premium * - * Note: the "premium.amount" may be`0` or more. + * "premium" cost (in excesses of the `baseCost`) of Ether measured in + * units of Wei paid to execute the "logical registrar action". + * + * May be 0. */ premium: PriceEth; /** - * Total cost for performing the registrar action. + * Total * - * Sum of `baseCost.amount` and `premium.amount`. + * Total cost of Ether measured in units of Wei paid to execute + * the "logical registrar action". * - * Note: the "total.amount" may be`0` or more. + * May be 0. */ total: PriceEth; } /** - * Prices information for performing the "logical" registrar action. + * Pricing information for a "logical registrar action" when + * registrar controller does not implement pricing. */ export interface RegistrarActionPricingNotApplicable { /** * Base cost * - * Always null, as `total` is null. + * Base cost (before any `premium`) of Ether measured in units of Wei + * paid to execute the "logical registrar action". */ baseCost: null; /** * Premium * - * Always null, as `total` is null. + * "premium" cost (in excesses of the `baseCost`) of Ether measured in + * units of Wei paid to execute the "logical registrar action". */ premium: null; /** - * Total cost for performing the registrar action. + * Total * - * Always null, as `baseCost` and `premium` are both null. + * Total cost of Ether measured in units of Wei paid to execute + * the "logical registrar action". */ total: null; } @@ -90,7 +101,7 @@ export function isRegistrarActionPricingAvailable( } /** - * Referrals information for performing the "logical" registrar action. + * * Referral information for performing a "logical registrar action". */ export interface RegistrarActionReferralAvailable { /** @@ -98,43 +109,44 @@ export interface RegistrarActionReferralAvailable { * * Represents the "raw" 32-byte "referrer" value emitted onchain in * association with the registrar action. - * - * If a registrar / registrar controller supports the concept of - * referrers then this field is set (non-null). */ encodedReferrer: EncodedReferrer; /** * Decoded Referrer * - * Represents ENSNode's subjective interpretation of - * {@link RegistrarAction.encodedReferrer}. + * Decoded referrer according to the subjective interpretation of + * `encodedReferrer` defined for ENS Holiday Awards. * - * Invariants: - * - If the first `12`-bytes of "encodedReferrer" are all `0`, - * then "decodedReferrer" is the last `20`-bytes of "encodedReferrer", - * else: "decodedReferrer" is the zero address. + * Identifies the interpreted address of the referrer. + * The "chainId" of this address is the same as is referenced in + * `subregistryId`. + * + * May be the "zero address" to represent that an `encodedReferrer` is + * defined but that it is interpreted as no referrer. */ decodedReferrer: Address; } /** - * Referrals information for performing the "logical" registrar action. + * Referral information for performing a "logical registrar action" when + * registrar controller does not implement referrals. */ export interface RegistrarActionReferralNotApplicable { /** * Encoded Referrer * - * Always null, as registrar / registrar controller doesn't support the concept of - * referrers. + * Represents the "raw" 32-byte "referrer" value emitted onchain in + * association with the registrar action. */ encodedReferrer: null; /** * Decoded Referrer * + * Decoded referrer according to the subjective interpretation of + * `encodedReferrer` defined for ENS Holiday Awards. * - * Always null, as `encodedReferrer` is null. */ decodedReferrer: null; } @@ -152,117 +164,161 @@ export function isRegistrarActionReferralAvailable( } /** - * "Logical" Registrar Action + * "Logical registrar action" * - * Represents a state of "logical" Registrar Action. May be built using data + * Represents a state of "logical registrar action". May be built using data * from multiple events within the same "logical" registration / renewal action. */ export interface RegistrarAction { /** - * Registrar Action ID + * "Logical registrar action" ID + * + * The `id` value is a deterministic and globally unique identifier for + * the "logical registrar action". + * + * The `id` value represents the *initial* onchain event associated with + * the "logical registrar action", but the full state of + * the "logical registrar action" is an aggregate across each of + * the onchain events referenced in the `eventIds` field. * - * This is ID of the event which initiated the "logical" Registrar Action. + * Guaranteed to be the very first element in `eventIds` array. */ id: RegistrarActionEventId; /** - * Registrar Action Type - * - * The type of the Registrar Action. + * The type of the "logical registrar action". */ type: RegistrarActionType; /** + * * Incremental Duration * - * Represents the incremental increase in the duration of the lifespan of - * the registration for `node` that was active as of `timestamp`. - * Measured in seconds. + * If `type` is "registration": + * - Represents the duration between `block.timestamp` and + * the initial `registrationLifecycle.expiresAt` value that the associated + * "registration lifecycle" will be initialized with. + * If `type` is "renewal": + * - Represents the incremental increase in duration made to + * the `registrationLifecycle.expiresAt` value in the associated + * "registration lifecycle". * - * A name with an active registration can be renewed at any time. + * A "registration lifecycle" may be extended via renewal even after it + * expires if it is still within its grace period. * - * Names that have expired may still be renewable. + * Consider the following scenario: * - * For example: assume the registration of a direct subname of Ethnames is - * scheduled to expire on Jan 1, midnight UTC. It is currently 30 days after - * this expiration time. Therefore, there are currently another 60 days of - * grace period remaining for this name. Anyone can still make - * a renewal of this name. + * The "registration lifecycle" of a direct subname of .eth is scheduled to + * expire on Jan 1, midnight UTC. It is currently 30 days after this + * expiration time. Therefore, there are currently another 60 days of grace + * period remaining for this name. Anyone can still make a renewal to + * extend the "registration lifecycle" of this name. * - * Consider the following scenarios for renewals of a name that - * has expired but is still within its grace period: + * Given this scenario, consider the following examples: * - * 1) Expired (in grace period) -> Expired (in grace period): - * If a renewal is made for 10 days incremental duration, - * this name remains in an "expired" (in grace period) state, but it now - * has 70 days of grace period remaining instead of only 60. + * 1. If a renewal is made with 10 days incremental duration, + * the "registration lifecycle" for this name will remain in + * an "expired" state, but it will now have another 70 days of + * grace period remaining. * - * 2) Expired (in grace period) -> Active: - * If a renewal is made for 50 days incremental duration, - * this name is no longer "expired" (in grace period) and is active, but it now - * expires and begins a new grace period in 20 days. + * 2. If a renewal is made with 50 days incremental duration, + * the "registration lifecycle" for this name will no longer be + * "expired" and will become "active", but the "registration lifecycle" + * will now be scheduled to expire again in 20 days. * - * After the latest registration of a direct subname becomes expired by - * more than the grace period, it can no longer be renewed by anyone. - * It must first be registered again, starting a new registration lifecycle of - * active / expiry / grace period / etc. + * After the "registration lifecycle" for a name becomes expired by more + * than its grace period, it can no longer be renewed by anyone and is + * considered "released". The name must first be registered again, starting + * a new "registration lifecycle" of + * active / expired / grace period / released. + * + * May be 0. + * + * Guaranteed to be a non-negative bigint value. */ incrementalDuration: Duration; /** * Registrant * - * Account that initiated the registrarAction and is paying the "total". - * It may not be the owner of the name: + * Identifies the address that initiated the "logical registrar action" and + * is paying the `pricing.total` cost (if applicable). * + * It may not be the owner of the name: * 1. When a name is registered, the initial owner of the name may be * distinct from the registrant. * 2. There are no restrictions on who may renew a name. * Therefore the owner of the name may be distinct from the registrant. + * + * The "chainId" of this address is the same as is referenced in + * `registrationLifecycle.subregistry.subregistryId`. */ registrant: Address; /** - * Registration Lifecycle that this "logical" Registrar Action was - * executed for. + * Registration Lifecycle associated with this "logical registrar action". */ registrationLifecycle: RegistrationLifecycle; /** - * Pricing information for performing this "logical" Registrar Action. + * Pricing information associated with this "logical registrar action". */ pricing: RegistrarActionPricing; /** - * Referral information related to performing this "logical" Registrar Action. + * Referral information associated with this "logical registrar action". */ referral: RegistrarActionReferral; /** * Block ref * - * References the block where "logical" Registrar Action was executed. + * References the block where the "logical registrar action" was executed. + * + * The "chainId" of this block is the same as is referenced in + * `registrationLifecycle.subregistry.subregistryId`. */ block: BlockRef; /** * Transaction hash * - * References the transaction within the `block` where - * the "logical" Registrar Action was executed. + * Transaction hash of the transaction associated with + * the "logical registrar action". + * + * The "chainId" of this transaction is the same as is referenced in + * `registrationLifecycle.subregistry.subregistryId`. + * + * Note that a single transaction may be associated with any number of + * "logical registrar actions". */ transactionHash: Hash; /** * Event IDs * - * An array of IDs referencing events which while being handled, - * contributed to the state of the "logical" Registrar Action. + * Array of the eventIds that have contributed to the state of + * the "logical registrar action" record. + * + * Each eventId is a deterministic and globally unique onchain event + * identifier. + * + * Guarantees: + * - Each eventId is of events that occurred within the block + * referenced by `block.number`. + * - At least 1 eventId. + * - Ordered chronologically (ascending) by logIndex within `block.number`. + * - The first element in the array is equal to the `id` of + * the overall "logical registrar action" record. * - * Guaranteed to: - * - Be ordered chronologically by event log index. - * - Have at least one element. - * - Reference the same value as `id` with its very first element. + * The following ideas are not generalized for ENS overall but happen to + * be a characteristic of the scope of our current indexing logic: + * 1. These id's always reference events emitted by + * a related "BaseRegistrar" contract. + * 2. These id's optionally reference events emitted by + * a related "Registrar Controller" contract. This is because our + * current indexing logic doesn't guarantee to index + * all "Registrar Controller" contracts. */ eventIds: [RegistrarActionEventId, ...RegistrarActionEventId[]]; } diff --git a/packages/ensnode-sdk/src/registrars/registration-lifecycle.ts b/packages/ensnode-sdk/src/registrars/registration-lifecycle.ts index dc33939194..de4ad765d6 100644 --- a/packages/ensnode-sdk/src/registrars/registration-lifecycle.ts +++ b/packages/ensnode-sdk/src/registrars/registration-lifecycle.ts @@ -2,7 +2,13 @@ import type { Node } from "../ens"; import type { UnixTimestamp } from "../shared"; import type { Subregistry } from "./subregistry"; -export const RegistrationLifecycleStages = { +/** + * Registration Lifecycle Stages + * + * Important: this definition should not be used anywhere. + * It's only here to capture some ideas that were shared in the team. + */ +const RegistrationLifecycleStages = { /** * Active * @@ -44,23 +50,23 @@ export type RegistrationLifecycleStage = */ export interface RegistrationLifecycle { /** - * Subregistry account that this Registration Lifecycle belongs to. + * Subregistry that manages this Registration Lifecycle. */ subregistry: Subregistry; /** - * The node of the FQDN of the domain this is associated with, - * guaranteed to be a subname of the associated subregistry - * for which the registration was executed. + * The node (namehash) of the FQDN of the domain the registration lifecycle + * is associated with. + * + * Guaranteed to be a subname of the node (namehash) of the subregistry + * identified by `subregistryId.subregistryId`. */ node: Node; /** * Expires at * - * The moment when the RegistrationLifecycle will transition - * from {@link RegistrationLifecycleStages.Active} - * to {@link RegistrationLifecycleStages.GracePeriod}. + * Identifies when the Registration Lifecycle is scheduled to expire. */ expiresAt: UnixTimestamp; } diff --git a/packages/ensnode-sdk/src/registrars/subregistry.ts b/packages/ensnode-sdk/src/registrars/subregistry.ts index d0b3b61a5a..f821d805b5 100644 --- a/packages/ensnode-sdk/src/registrars/subregistry.ts +++ b/packages/ensnode-sdk/src/registrars/subregistry.ts @@ -6,10 +6,12 @@ import type { AccountId } from "../shared"; */ export interface Subregistry { /** - * Subregistry Account ID + * Subregistry ID * - * Identifies the account of the smart contract associated - * with the subregistry. + * The ID of the subregistry the "logical registrar action" was taken on. + * + * Identifies the chainId and address of the associated subregistry smart + * contract. */ subregistryId: AccountId; diff --git a/packages/ensnode-sdk/src/shared/datetime.test.ts b/packages/ensnode-sdk/src/shared/datetime.test.ts index 6f97205e74..cc444b0a1d 100644 --- a/packages/ensnode-sdk/src/shared/datetime.test.ts +++ b/packages/ensnode-sdk/src/shared/datetime.test.ts @@ -6,9 +6,10 @@ describe("datetime", () => { describe("durationBetween()", () => { it("returns duration for valid input where start is before end", () => { expect(durationBetween(1234, 4321)).toEqual(3087); + expect(durationBetween(1234, 1234)).toEqual(0); }); it("throws an error for invalid input where end is before start", () => { - expect(() => durationBetween(4321, 1234)).toThrowError( + expect(() => durationBetween(1234, 1233)).toThrowError( /Duration must be a non-negative integer/i, ); }); diff --git a/packages/ensnode-sdk/src/shared/numbers.test.ts b/packages/ensnode-sdk/src/shared/numbers.test.ts index f6fce25a6b..af5fbaa07d 100644 --- a/packages/ensnode-sdk/src/shared/numbers.test.ts +++ b/packages/ensnode-sdk/src/shared/numbers.test.ts @@ -3,18 +3,22 @@ import { describe, expect, it } from "vitest"; import { bigIntToNumber } from "./numbers"; describe("Numbers", () => { - it("can convert bigint to number when possible", () => { - expect(bigIntToNumber(BigInt(Number.MAX_SAFE_INTEGER))).toEqual(Number.MAX_SAFE_INTEGER); - }); + describe("bigIntToNumber()", () => { + it("can convert bigint to number when possible", () => { + expect(bigIntToNumber(BigInt(Number.MIN_SAFE_INTEGER))).toEqual(Number.MIN_SAFE_INTEGER); - it("refuses to convert to low bigint value", () => { - expect(() => bigIntToNumber(BigInt(Number.MIN_SAFE_INTEGER - 1))).toThrowError( - /The bigint '-9007199254740992' value is too low to be to converted into a number/i, - ); - }); - it("refuses to convert to high bigint value", () => { - expect(() => bigIntToNumber(BigInt(Number.MAX_SAFE_INTEGER + 1))).toThrowError( - /The bigint '9007199254740992' value is too high to be to converted into a number/i, - ); + expect(bigIntToNumber(BigInt(Number.MAX_SAFE_INTEGER))).toEqual(Number.MAX_SAFE_INTEGER); + }); + + it("refuses to convert to low bigint value", () => { + expect(() => bigIntToNumber(BigInt(Number.MIN_SAFE_INTEGER - 1))).toThrowError( + /The bigint '-9007199254740992' value is too low to be to converted into a number/i, + ); + }); + it("refuses to convert to high bigint value", () => { + expect(() => bigIntToNumber(BigInt(Number.MAX_SAFE_INTEGER + 1))).toThrowError( + /The bigint '9007199254740992' value is too high to be to converted into a number/i, + ); + }); }); }); diff --git a/packages/ensnode-sdk/src/shared/numbers.ts b/packages/ensnode-sdk/src/shared/numbers.ts index d788771d31..b37f0a3287 100644 --- a/packages/ensnode-sdk/src/shared/numbers.ts +++ b/packages/ensnode-sdk/src/shared/numbers.ts @@ -1,8 +1,8 @@ /** * Converts a bigint value into a number value. * - * @throws when value is too low. - * @throws when value is too high . + * @throws when value is outside the range of `Number.MIN_SAFE_INTEGER` and + * `Number.MAX_SAFE_INTEGER`. */ export function bigIntToNumber(n: bigint): number { if (n < Number.MIN_SAFE_INTEGER) { From 182c43bdecfa66c03fdba4a3c7ae58146725e939 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 6 Nov 2025 18:35:30 +0100 Subject: [PATCH 12/13] docs(changeset): Introduces a new `registrars` plugin for tracking all registrations and renewals for direct subnames of `eth`, `base.eth`, and `linea.eth`. --- .changeset/quiet-yaks-sleep.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/quiet-yaks-sleep.md diff --git a/.changeset/quiet-yaks-sleep.md b/.changeset/quiet-yaks-sleep.md new file mode 100644 index 0000000000..9adcdd5f96 --- /dev/null +++ b/.changeset/quiet-yaks-sleep.md @@ -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`. From 0b4b32be080504ffdd4ce27f4cd8729a56657708 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 6 Nov 2025 19:21:12 +0100 Subject: [PATCH 13/13] apply pr feedback Rename `RegistrarActionPricingNotApplicable` type to be `RegistrarActionPricingUnknown` --- .../basenames/handlers/Basenames_RegistrarController.ts | 6 ++++-- packages/ensnode-sdk/src/registrars/registrar-action.ts | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) 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 6278486c8b..35a45b610f 100644 --- a/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/basenames/handlers/Basenames_RegistrarController.ts @@ -7,7 +7,7 @@ import { DatasourceNames } from "@ensnode/datasources"; import { makeSubdomainNode, PluginName, - type RegistrarActionPricingNotApplicable, + type RegistrarActionPricingUnknown, type RegistrarActionReferralNotApplicable, } from "@ensnode/ensnode-sdk"; @@ -34,12 +34,14 @@ export default function () { * 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 RegistrarActionPricingNotApplicable; + } satisfies RegistrarActionPricingUnknown; /** * No Registrar Controller for Basenames implements referrals or diff --git a/packages/ensnode-sdk/src/registrars/registrar-action.ts b/packages/ensnode-sdk/src/registrars/registrar-action.ts index d0f3148a3c..3de389dd57 100644 --- a/packages/ensnode-sdk/src/registrars/registrar-action.ts +++ b/packages/ensnode-sdk/src/registrars/registrar-action.ts @@ -60,9 +60,9 @@ export interface RegistrarActionPricingAvailable { /** * Pricing information for a "logical registrar action" when - * registrar controller does not implement pricing. + * there is no known pricing data. */ -export interface RegistrarActionPricingNotApplicable { +export interface RegistrarActionPricingUnknown { /** * Base cost * @@ -90,7 +90,7 @@ export interface RegistrarActionPricingNotApplicable { export type RegistrarActionPricing = | RegistrarActionPricingAvailable - | RegistrarActionPricingNotApplicable; + | RegistrarActionPricingUnknown; export function isRegistrarActionPricingAvailable( registrarActionPricing: RegistrarActionPricing,