diff --git a/.changeset/modern-groups-occur.md b/.changeset/modern-groups-occur.md new file mode 100644 index 0000000000..c3be157c48 --- /dev/null +++ b/.changeset/modern-groups-occur.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-sdk": patch +--- + +Aligned `isRegistrationFullyExpired` and `isRegistrationInGracePeriod` helpers with onchain logic. diff --git a/.changeset/two-corners-tease.md b/.changeset/two-corners-tease.md new file mode 100644 index 0000000000..8be49f5941 --- /dev/null +++ b/.changeset/two-corners-tease.md @@ -0,0 +1,5 @@ +--- +"ensindexer": patch +--- + +Updated ENSv2Registry handling for non-expiring reverse-name registrations. diff --git a/apps/ensindexer/src/plugins/unigraph/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/unigraph/handlers/ensv2/ENSv2Registry.ts index 2aefcd4f87..ab3840b5ee 100644 --- a/apps/ensindexer/src/plugins/unigraph/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/unigraph/handlers/ensv2/ENSv2Registry.ts @@ -1,6 +1,7 @@ import { type AccountId, asLiteralLabel, + isNormalizedAddress, type LabelHash, labelhashLiteralLabel, makeENSv2DomainId, @@ -103,10 +104,21 @@ export default function () { `Invariant(ENSv2Registry:Label[Registered|Reserved]): Existing unexpired Registration found, expected none or expired.\n${toJson(registration, { pretty: true })}`, ); } - } else { - // Invariant: if this is a Registration, unless it is a Reservation, it should be fully expired + } else if (registration) { + // There's an existing Registration record, so we need to handle it carefully. + // Registrations for reverse names are a special case, since they can + // never expire. Therefore, we need to skip the expiration check for + // reverse name registrations and allow a new Registration record to be + // created for the same label. + // For a reverse name registration, the registrant ID is the label with `0x` prefix. + const maybeReverseNameLabel = `0x${label}`; + const isReverseNameRegistration = + isNormalizedAddress(maybeReverseNameLabel) && + registration.registrantId === maybeReverseNameLabel; + + // Invariant: if this is a Registration, unless it is a Reservation or a reverse name Registration, it should be fully expired if ( - registration && + !isReverseNameRegistration && registration.type !== "ENSv2RegistryReservation" && !isRegistrationFullyExpired(registration, event.block.timestamp) ) { diff --git a/packages/ensnode-sdk/src/registrars/registration-expiration.test.ts b/packages/ensnode-sdk/src/registrars/registration-expiration.test.ts new file mode 100644 index 0000000000..857f797592 --- /dev/null +++ b/packages/ensnode-sdk/src/registrars/registration-expiration.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; + +import { + isRegistrationExpired, + isRegistrationFullyExpired, + isRegistrationInGracePeriod, +} from "./registration-expiration"; + +describe("registration expiration", () => { + const expiry = 1000n; + const gracePeriod = 100n; + + describe("isRegistrationExpired", () => { + it.each([ + { now: 999n, expected: false, description: "before expiry" }, + { now: 1000n, expected: true, description: "at expiry" }, + { now: 1001n, expected: true, description: "after expiry" }, + ])("returns $expected when $description", ({ now, expected }) => { + expect(isRegistrationExpired({ expiry, gracePeriod }, now)).toBe(expected); + }); + + it("returns false when expiry is null", () => { + expect(isRegistrationExpired({ expiry: null, gracePeriod }, 2000n)).toBe(false); + }); + }); + + describe("isRegistrationFullyExpired", () => { + it.each([ + { now: 999n, expected: false, description: "before expiry" }, + { now: 1000n, expected: false, description: "at expiry" }, + { now: 1050n, expected: false, description: "during grace period" }, + { now: 1100n, expected: false, description: "at expiry + grace period" }, + { now: 1101n, expected: true, description: "after expiry + grace period" }, + ])("returns $expected when $description", ({ now, expected }) => { + expect(isRegistrationFullyExpired({ expiry, gracePeriod }, now)).toBe(expected); + }); + + it("returns false when expiry is null", () => { + expect(isRegistrationFullyExpired({ expiry: null, gracePeriod }, 2000n)).toBe(false); + }); + + it("treats null grace period as zero", () => { + expect(isRegistrationFullyExpired({ expiry, gracePeriod: null }, 1000n)).toBe(false); + expect(isRegistrationFullyExpired({ expiry, gracePeriod: null }, 1001n)).toBe(true); + }); + }); + + describe("isRegistrationInGracePeriod", () => { + it.each([ + { now: 999n, expected: false, description: "before expiry" }, + { now: 1000n, expected: true, description: "at expiry" }, + { now: 1050n, expected: true, description: "during grace period" }, + { now: 1100n, expected: true, description: "at expiry + grace period" }, + { now: 1101n, expected: false, description: "after expiry + grace period" }, + ])("returns $expected when $description", ({ now, expected }) => { + expect(isRegistrationInGracePeriod({ expiry, gracePeriod }, now)).toBe(expected); + }); + + it("returns false when expiry is null", () => { + expect(isRegistrationInGracePeriod({ expiry: null, gracePeriod }, 1050n)).toBe(false); + }); + + it("returns false when grace period is null", () => { + expect(isRegistrationInGracePeriod({ expiry, gracePeriod: null }, 1050n)).toBe(false); + }); + }); +}); diff --git a/packages/ensnode-sdk/src/registrars/registration-expiration.ts b/packages/ensnode-sdk/src/registrars/registration-expiration.ts index b81e7d696e..a046c08688 100644 --- a/packages/ensnode-sdk/src/registrars/registration-expiration.ts +++ b/packages/ensnode-sdk/src/registrars/registration-expiration.ts @@ -23,8 +23,8 @@ export function isRegistrationFullyExpired(info: RegistrationExpiryInfo, now: bi // no expiry, never expired if (info.expiry == null) return false; - // otherwise it is expired if now >= expiry + grace - return now >= info.expiry + (info.gracePeriod ?? 0n); + // otherwise it is expired if now > expiry + grace + return now > info.expiry + (info.gracePeriod ?? 0n); } /** @@ -34,5 +34,5 @@ export function isRegistrationInGracePeriod(info: RegistrationExpiryInfo, now: b if (info.expiry == null) return false; if (info.gracePeriod == null) return false; - return info.expiry <= now && info.expiry + info.gracePeriod > now; + return info.expiry <= now && info.expiry + info.gracePeriod >= now; }