From 6aca12e4a917e841b0b9190a10c99c432f586435 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 21 Apr 2026 17:15:25 +0200 Subject: [PATCH 01/12] Add `Referrer` type to `enssdk` --- packages/enssdk/src/lib/types/evm.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/enssdk/src/lib/types/evm.ts b/packages/enssdk/src/lib/types/evm.ts index 7a0df657a..21f118c2c 100644 --- a/packages/enssdk/src/lib/types/evm.ts +++ b/packages/enssdk/src/lib/types/evm.ts @@ -21,6 +21,15 @@ export type Address = ViemAddress; */ export type NormalizedAddress = Address; +/** + * Referrer + * + * Represents a raw 32-byte onchain referrer value as emitted by ENS Registrar Controller contracts. + * + * @invariant Guaranteed to be a hex string representation of a 32-byte value. + */ +export type Referrer = Hex; + /** * Unix timestamp value as bigint. * From 26749bbef1684044fe0dd4a9375b2bbb0c02dcae Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 21 Apr 2026 17:16:28 +0200 Subject: [PATCH 02/12] Integrate `Referrer` type in ENSDb SDK --- packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts index 828112322..2f9660b13 100644 --- a/packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts +++ b/packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts @@ -9,6 +9,7 @@ import type { PermissionsId, PermissionsResourceId, PermissionsUserId, + Referrer, RegistrationId, RegistryId, RenewalId, @@ -17,8 +18,6 @@ import type { import { index, onchainEnum, onchainTable, primaryKey, relations, sql, uniqueIndex } from "ponder"; import type { BlockNumber, Hash } from "viem"; -import type { EncodedReferrer } from "@ensnode/ensnode-sdk"; - /** * The ENSv2 Schema * @@ -361,7 +360,7 @@ export const registration = onchainTable( unregistrantId: t.hex().$type
(), // may have referrer data - referrer: t.hex().$type(), + referrer: t.hex().$type(), // may have fuses (NameWrapper, Wrapped BaseRegistrar) fuses: t.integer(), @@ -443,7 +442,7 @@ export const renewal = onchainTable( duration: t.bigint().notNull(), // may have a referrer - referrer: t.hex().$type(), + referrer: t.hex().$type(), // TODO(paymentToken): add payment token tracking here From 38e1aa877d750792e9e04d59405981c52828ab2c Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 21 Apr 2026 17:17:47 +0200 Subject: [PATCH 03/12] Move `encoded-referrer.ts` file from `ensnode-sdk` to `enssdk` --- packages/ens-referrals/README.md | 17 ++++++- .../src}/encoded-referrer.test.ts | 20 +++++--- .../src}/encoded-referrer.ts | 48 +++++++++++-------- packages/ens-referrals/src/index.ts | 1 + packages/ensnode-sdk/src/registrars/index.ts | 1 - 5 files changed, 57 insertions(+), 30 deletions(-) rename packages/{ensnode-sdk/src/registrars => ens-referrals/src}/encoded-referrer.test.ts (80%) rename packages/{ensnode-sdk/src/registrars => ens-referrals/src}/encoded-referrer.ts (51%) diff --git a/packages/ens-referrals/README.md b/packages/ens-referrals/README.md index 394acad87..cd1246543 100644 --- a/packages/ens-referrals/README.md +++ b/packages/ens-referrals/README.md @@ -154,7 +154,9 @@ Check out [`production-editions.json`](https://ensawards.org/production-editions ## Other Utilities -The package also includes helpers for building referral links. +The package also includes helpers. + +### Building referral links ```typescript import { buildEnsReferralUrl } from "@namehash/ens-referrals"; @@ -166,3 +168,16 @@ const referrerAddress: Address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; const referrerUrl = buildEnsReferralUrl(referrerAddress).toString(); // https://app.ens.domains/?referrer=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 ``` + +### Building encoded referrer + +```typescript +import { buildEncodedReferrer } from "@namehash/ens-referrals"; +import type { Address } from "enssdk"; + +const referrerAddress: Address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + +// Build an encoded referrer value +const encodedReferrer = buildEncodedReferrer(referrerAddress); +// 0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045 +``` diff --git a/packages/ensnode-sdk/src/registrars/encoded-referrer.test.ts b/packages/ens-referrals/src/encoded-referrer.test.ts similarity index 80% rename from packages/ensnode-sdk/src/registrars/encoded-referrer.test.ts rename to packages/ens-referrals/src/encoded-referrer.test.ts index bcbbc5475..40fdfdd43 100644 --- a/packages/ensnode-sdk/src/registrars/encoded-referrer.test.ts +++ b/packages/ens-referrals/src/encoded-referrer.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; import { buildEncodedReferrer, - decodeEncodedReferrer, + decodeReferrer, ENCODED_REFERRER_BYTE_LENGTH, ENCODED_REFERRER_BYTE_OFFSET, } from "./encoded-referrer"; @@ -24,7 +24,7 @@ describe("encoded referrer", () => { const input = pad(vitalikEthAddressLowercase); // act - const result = decodeEncodedReferrer(input); + const result = decodeReferrer(input); // assert expect(result).toEqual(vitalikEthAddressLowercase); @@ -37,7 +37,7 @@ describe("encoded referrer", () => { const input = concat([initialBytes, vitalikEthAddressLowercase]); // act - const result = decodeEncodedReferrer(input); + const result = decodeReferrer(input); // & assert expect(result).toStrictEqual(zeroAddress); }); @@ -47,7 +47,7 @@ describe("encoded referrer", () => { const input = pad("0xzzzzzzzzzzzzzzzzzzzz"); // act & assert - expect(() => decodeEncodedReferrer(input)).toThrowError( + expect(() => decodeReferrer(input)).toThrowError( /Decoded referrer value must be a valid EVM address/i, ); }); @@ -55,7 +55,7 @@ describe("encoded referrer", () => { describe("decoding a non-32-byte value", () => { it("throws an error", () => { - expect(() => decodeEncodedReferrer("0x")).toThrowError( + expect(() => decodeReferrer("0x")).toThrowError( /Encoded referrer value must be represented by 32 bytes/i, ); }); @@ -72,13 +72,19 @@ describe("encoded referrer", () => { dir: "left", }); - const encodedReferrer = buildEncodedReferrer(toNormalizedAddress(address)); + const encodedReferrer = buildEncodedReferrer(address); expect(encodedReferrer).toEqual(expectedEncodedReferrer); // decoding should operate as expected const expectedDecodedReferrer = toNormalizedAddress(address); - const decodedReferrer = decodeEncodedReferrer(encodedReferrer); + const decodedReferrer = decodeReferrer(encodedReferrer); expect(decodedReferrer).toStrictEqual(expectedDecodedReferrer); }); }); + + it("throws an error when building encoded referrer with invalid EVM address", () => { + expect(() => buildEncodedReferrer("0xnotavalidaddress" as Address)).toThrowError( + /'0xnotavalidaddress' does not represent an EVM Address/i, + ); + }); }); diff --git a/packages/ensnode-sdk/src/registrars/encoded-referrer.ts b/packages/ens-referrals/src/encoded-referrer.ts similarity index 51% rename from packages/ensnode-sdk/src/registrars/encoded-referrer.ts rename to packages/ens-referrals/src/encoded-referrer.ts index 0943833f3..53f3b34e6 100644 --- a/packages/ensnode-sdk/src/registrars/encoded-referrer.ts +++ b/packages/ens-referrals/src/encoded-referrer.ts @@ -1,29 +1,30 @@ -import { type Hex, isNormalizedAddress, type NormalizedAddress, toNormalizedAddress } from "enssdk"; +import type { Address, Referrer } from "enssdk"; +import { type Hex, type NormalizedAddress, toNormalizedAddress } from "enssdk"; import { pad, size, slice, zeroAddress } from "viem"; /** * Encoded Referrer * - * Represents a "raw" ENS referrer value. + * Represents "a Referrer that is guaranteed to be validly encoded" * - * Registrar controllers emit referrer data as bytes32 values. This type represents - * that raw 32-byte hex string. + * Guaranteed to be a 32-byte hex with 12 bytes of zero padding followed by + * a 20-byte lowercase address. * - * @invariant Guaranteed to be a hex string representation of a 32-byte value. + * Constructible only through {@link buildEncodedReferrer} (and {@link ZERO_ENCODED_REFERRER}). */ -export type EncodedReferrer = Hex; +export type EncodedReferrer = Referrer & { readonly __brand: "EncodedReferrer" }; /** * Encoded Referrer byte offset * - * The count of left-padded bytes in an {@link EncodedReferrer} value. + * The count of left-padded bytes in an {@link Referrer} value. */ export const ENCODED_REFERRER_BYTE_OFFSET = 12; /** * Encoded Referrer byte length * - * The count of bytes the {@link EncodedReferrer} value consists of. + * The count of bytes the {@link Referrer} value consists of. */ export const ENCODED_REFERRER_BYTE_LENGTH = 32; @@ -43,46 +44,51 @@ export const EXPECTED_ENCODED_REFERRER_PADDING: Hex = pad("0x", { * * Guaranteed to be a hex string representation of a 32-byte zero value. */ -export const ZERO_ENCODED_REFERRER: EncodedReferrer = pad("0x", { +export const ZERO_ENCODED_REFERRER = pad("0x", { size: ENCODED_REFERRER_BYTE_LENGTH, dir: "left", -}); +}) as EncodedReferrer; /** - * Build an {@link EncodedReferrer} value for the given {@link NormalizedAddress} + * Build an {@link EncodedReferrer} value for the given {@link Address} * according to the referrer encoding with left-zero-padding. + * + * @throws if `address` does not represent an EVM Address */ -export function buildEncodedReferrer(address: NormalizedAddress): EncodedReferrer { - if (!isNormalizedAddress(address)) throw new Error(`Address '${address}' is not normalized.`); +export function buildEncodedReferrer(address: Address): EncodedReferrer { + const normalizedAddress = toNormalizedAddress(address); - return pad(address, { size: ENCODED_REFERRER_BYTE_LENGTH, dir: "left" }); + return pad(normalizedAddress, { + size: ENCODED_REFERRER_BYTE_LENGTH, + dir: "left", + }) as EncodedReferrer; } /** - * Decode an {@link EncodedReferrer} value into a {@link NormalizedAddress} + * Decode an {@link Referrer} value into a {@link NormalizedAddress} * according to the referrer encoding with left-zero-padding. * - * @param encodedReferrer - The "raw" {@link EncodedReferrer} value to decode. + * @param referrer - The "raw" {@link Referrer} value to decode. * @returns The decoded referrer address. - * @throws when encodedReferrer value is not represented by + * @throws when referrer value is not represented by * {@link ENCODED_REFERRER_BYTE_LENGTH} bytes. * @throws when decodedReferrer is not a valid EVM address. */ -export function decodeEncodedReferrer(encodedReferrer: EncodedReferrer): NormalizedAddress { +export function decodeReferrer(referrer: Referrer): NormalizedAddress { // Invariant: encoded referrer must be of expected size - if (size(encodedReferrer) !== ENCODED_REFERRER_BYTE_LENGTH) { + if (size(referrer) !== ENCODED_REFERRER_BYTE_LENGTH) { throw new Error( `Encoded referrer value must be represented by ${ENCODED_REFERRER_BYTE_LENGTH} bytes.`, ); } - const padding = slice(encodedReferrer, 0, ENCODED_REFERRER_BYTE_OFFSET); + const padding = slice(referrer, 0, ENCODED_REFERRER_BYTE_OFFSET); // strict validation: padding must be all zeros // if any byte in the padding is non-zero, treat as Zero Encoded Referrer if (padding !== EXPECTED_ENCODED_REFERRER_PADDING) return zeroAddress; - const decodedReferrer = slice(encodedReferrer, ENCODED_REFERRER_BYTE_OFFSET); + const decodedReferrer = slice(referrer, ENCODED_REFERRER_BYTE_OFFSET); try { // return normalized address diff --git a/packages/ens-referrals/src/index.ts b/packages/ens-referrals/src/index.ts index bb09aced5..70e320777 100644 --- a/packages/ens-referrals/src/index.ts +++ b/packages/ens-referrals/src/index.ts @@ -31,6 +31,7 @@ export * from "./client"; export * from "./edition"; export * from "./edition-metrics"; export * from "./edition-summary"; +export * from "./encoded-referrer"; export * from "./leaderboard"; export * from "./leaderboard-page"; export * from "./link"; diff --git a/packages/ensnode-sdk/src/registrars/index.ts b/packages/ensnode-sdk/src/registrars/index.ts index 24f9682c4..b9500096c 100644 --- a/packages/ensnode-sdk/src/registrars/index.ts +++ b/packages/ensnode-sdk/src/registrars/index.ts @@ -1,5 +1,4 @@ export * from "./basenames-subregistry"; -export * from "./encoded-referrer"; export * from "./ethnames-subregistry"; export * from "./lineanames-subregistry"; export * from "./registrar-action"; From bc0627437cf8ca39fb0770a64407a6c203b77465 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 21 Apr 2026 17:22:09 +0200 Subject: [PATCH 04/12] Update ENSNode SDK package Uses data model from `enssdk`. Also, as a workaround, defines an inline copy for the `decodeReferrer` function that cannot be imported from `@namehash/ens-referrals` package due to circular references issue. --- .../src/registrars/registrar-action.ts | 11 +--- .../ensnode-sdk/src/registrars/zod-schemas.ts | 64 ++++++++++++++++++- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/packages/ensnode-sdk/src/registrars/registrar-action.ts b/packages/ensnode-sdk/src/registrars/registrar-action.ts index c6edbdd1f..7e5bbddb4 100644 --- a/packages/ensnode-sdk/src/registrars/registrar-action.ts +++ b/packages/ensnode-sdk/src/registrars/registrar-action.ts @@ -1,11 +1,6 @@ -import type { Address, Duration } from "enssdk"; +import type { Address, Duration, NormalizedAddress, Referrer } from "enssdk"; import type { Hash } from "viem"; -import type { EncodedReferrer } from "./encoded-referrer"; - -export type { EncodedReferrer } from "./encoded-referrer"; -export { decodeEncodedReferrer, ZERO_ENCODED_REFERRER } from "./encoded-referrer"; - import type { PriceEth, SerializedPriceEth } from "../shared/currencies"; import { serializePriceEth } from "../shared/serialize"; import type { BlockRef } from "../shared/types"; @@ -114,7 +109,7 @@ export interface RegistrarActionReferralAvailable { * Represents the "raw" 32-byte "referrer" value emitted onchain in * association with the registrar action. */ - encodedReferrer: EncodedReferrer; + encodedReferrer: Referrer; /** * Decoded Referrer @@ -129,7 +124,7 @@ export interface RegistrarActionReferralAvailable { * May be the "zero address" to represent that an `encodedReferrer` is * defined but that it is interpreted as no referrer. */ - decodedReferrer: Address; + decodedReferrer: NormalizedAddress; } /** diff --git a/packages/ensnode-sdk/src/registrars/zod-schemas.ts b/packages/ensnode-sdk/src/registrars/zod-schemas.ts index ff6cca19a..f9ceacbcd 100644 --- a/packages/ensnode-sdk/src/registrars/zod-schemas.ts +++ b/packages/ensnode-sdk/src/registrars/zod-schemas.ts @@ -1,3 +1,5 @@ +import { type Hex, type NormalizedAddress, type Referrer, toNormalizedAddress } from "enssdk"; +import { pad, size, slice, zeroAddress } from "viem"; import { z } from "zod/v4"; import type { ParsePayload } from "zod/v4/core"; @@ -13,7 +15,6 @@ import { makeTransactionHashSchema, makeUnixTimestampSchema, } from "../shared/zod-schemas"; -import { decodeEncodedReferrer, ENCODED_REFERRER_BYTE_LENGTH } from "./encoded-referrer"; import { type RegistrarAction, type RegistrarActionEventId, @@ -26,6 +27,65 @@ import { import type { RegistrationLifecycle } from "./registration-lifecycle"; import { Subregistry } from "./subregistry"; +/** + * Encoded Referrer byte offset + * + * The count of left-padded bytes in an {@link Referrer} value. + */ +export const ENCODED_REFERRER_BYTE_OFFSET = 12; + +/** + * Encoded Referrer byte length + * + * The count of bytes the {@link Referrer} value consists of. + */ +export const ENCODED_REFERRER_BYTE_LENGTH = 32; + +/** + * Expected padding for a valid encoded referrer + * + * Properly encoded referrers must have exactly 12 zero bytes of left padding + * before the 20-byte Ethereum address. + */ +export const EXPECTED_ENCODED_REFERRER_PADDING: Hex = pad("0x", { + size: ENCODED_REFERRER_BYTE_OFFSET, + dir: "left", +}); + +/** + * Decode an {@link Referrer} value into a {@link NormalizedAddress} + * according to the referrer encoding with left-zero-padding. + * + * @param referrer - The "raw" {@link Referrer} value to decode. + * @returns The decoded referrer address. + * @throws when referrer value is not represented by + * {@link ENCODED_REFERRER_BYTE_LENGTH} bytes. + * @throws when decodedReferrer is not a valid EVM address. + */ +function decodeReferrer(referrer: Referrer): NormalizedAddress { + // Invariant: encoded referrer must be of expected size + if (size(referrer) !== ENCODED_REFERRER_BYTE_LENGTH) { + throw new Error( + `Encoded referrer value must be represented by ${ENCODED_REFERRER_BYTE_LENGTH} bytes.`, + ); + } + + const padding = slice(referrer, 0, ENCODED_REFERRER_BYTE_OFFSET); + + // strict validation: padding must be all zeros + // if any byte in the padding is non-zero, treat as Zero Encoded Referrer + if (padding !== EXPECTED_ENCODED_REFERRER_PADDING) return zeroAddress; + + const decodedReferrer = slice(referrer, ENCODED_REFERRER_BYTE_OFFSET); + + try { + // return normalized address + return toNormalizedAddress(decodedReferrer); + } catch { + throw new Error(`Decoded referrer value must be a valid EVM address.`); + } +} + /** * Schema for parsing objects into {@link Subregistry}. */ @@ -93,7 +153,7 @@ function invariant_registrarActionDecodedReferrerBasedOnRawReferrer( const { encodedReferrer, decodedReferrer } = ctx.value; try { - const expectedDecodedReferrer = decodeEncodedReferrer(encodedReferrer); + const expectedDecodedReferrer = decodeReferrer(encodedReferrer); if (decodedReferrer !== expectedDecodedReferrer) { ctx.issues.push({ From 24f2c1d33ed20be253d2ab2caa22bc53ca656322 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 21 Apr 2026 17:23:49 +0200 Subject: [PATCH 05/12] Namehash UI: import `ZERO_ENCODED_REFERRER` from `@namehash/ens-referrals` Replaces the ENSNode SDK import --- packages/namehash-ui/package.json | 1 + .../src/components/registrar-actions/RegistrarActionCard.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/namehash-ui/package.json b/packages/namehash-ui/package.json index 810cc5375..626fefcd0 100644 --- a/packages/namehash-ui/package.json +++ b/packages/namehash-ui/package.json @@ -80,6 +80,7 @@ "dependencies": { "@ensnode/datasources": "workspace:*", "@ensnode/ensnode-sdk": "workspace:*", + "@namehash/ens-referrals": "workspace:*", "enssdk": "workspace:*", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-slot": "^1.2.3", diff --git a/packages/namehash-ui/src/components/registrar-actions/RegistrarActionCard.tsx b/packages/namehash-ui/src/components/registrar-actions/RegistrarActionCard.tsx index dc377a896..79b9a96ec 100644 --- a/packages/namehash-ui/src/components/registrar-actions/RegistrarActionCard.tsx +++ b/packages/namehash-ui/src/components/registrar-actions/RegistrarActionCard.tsx @@ -1,3 +1,4 @@ +import { ZERO_ENCODED_REFERRER } from "@namehash/ens-referrals"; import type { Address, DefaultableChainId, UnixTimestamp } from "enssdk"; import { Info as InfoIcon, CircleQuestionMark as QuestionmarkIcon } from "lucide-react"; import { memo, type PropsWithChildren, type ReactNode } from "react"; @@ -12,7 +13,6 @@ import { buildUnresolvedIdentity, isRegistrarActionReferralAvailable, RegistrarActionTypes, - ZERO_ENCODED_REFERRER, } from "@ensnode/ensnode-sdk"; import { useIsMobile } from "../../hooks/useIsMobile"; From a4caa18d625ed68d5fcefebbc012b95db4568fb6 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 21 Apr 2026 17:25:11 +0200 Subject: [PATCH 06/12] ENSIndexer: import from `@namehash/ens-referrals` and `enssdk` Replaces the ENSNode SDK imports --- apps/ensindexer/package.json | 1 + .../ensv2/handlers/ensv1/RegistrarController.ts | 7 ++++--- .../src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts | 12 ++++-------- .../handlers/Ethnames_RegistrarController.ts | 6 +++--- ...Ethnames_UniversalRegistrarRenewalWithReferrer.ts | 9 +++------ .../shared/lib/registrar-controller-events.ts | 5 ++--- ...iversal-registrar-renewal-with-referrer-events.ts | 5 ++--- 7 files changed, 19 insertions(+), 26 deletions(-) diff --git a/apps/ensindexer/package.json b/apps/ensindexer/package.json index 7de460af7..44205190b 100644 --- a/apps/ensindexer/package.json +++ b/apps/ensindexer/package.json @@ -28,6 +28,7 @@ "@ensnode/ensnode-sdk": "workspace:*", "@ensnode/ensrainbow-sdk": "workspace:*", "@ensnode/ponder-sdk": "workspace:*", + "@namehash/ens-referrals": "workspace:*", "@ponder/client": "catalog:", "caip": "catalog:", "date-fns": "catalog:", diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts index a94df08e7..1e99be21c 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts @@ -7,9 +7,10 @@ import { labelhashLiteralLabel, makeENSv1DomainId, makeSubdomainNode, + type Referrer, } from "enssdk"; -import { type EncodedReferrer, PluginName } from "@ensnode/ensnode-sdk"; +import { PluginName } from "@ensnode/ensnode-sdk"; import { ensureDomainEvent } from "@/lib/ensv2/event-db-helpers"; import { ensureLabel, ensureUnknownLabel } from "@/lib/ensv2/label-db-helpers"; @@ -38,7 +39,7 @@ export default function () { labelHash: LabelHash; baseCost?: bigint; premium?: bigint; - referrer?: EncodedReferrer; + referrer?: Referrer; }>; }) { const { labelHash, baseCost: base, premium, referrer } = event.args; @@ -91,7 +92,7 @@ export default function () { labelHash: LabelHash; baseCost?: bigint; premium?: bigint; - referrer?: EncodedReferrer; + referrer?: Referrer; }>; }) { const { labelHash, baseCost: base, premium, referrer } = event.args; diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts index d484bdf90..d36834e8a 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts @@ -4,17 +4,13 @@ import { makeENSv2DomainId, makeStorageId, type NormalizedAddress, + type Referrer, type TokenId, type UnixTimestampBigInt, type Wei, } from "enssdk"; -import { - type EncodedReferrer, - interpretAddress, - isRegistrationFullyExpired, - PluginName, -} from "@ensnode/ensnode-sdk"; +import { interpretAddress, isRegistrationFullyExpired, PluginName } from "@ensnode/ensnode-sdk"; import { ensureAccount } from "@/lib/ensv2/account-db-helpers"; import { ensureDomainEvent, ensureEvent } from "@/lib/ensv2/event-db-helpers"; @@ -61,7 +57,7 @@ export default function () { subregistry: NormalizedAddress; resolver: NormalizedAddress; duration: DurationBigInt; - referrer: EncodedReferrer; + referrer: Referrer; paymentToken: NormalizedAddress; base: Wei; premium: Wei; @@ -138,7 +134,7 @@ export default function () { label: string; duration: DurationBigInt; newExpiry: UnixTimestampBigInt; - referrer: EncodedReferrer; + referrer: Referrer; paymentToken: NormalizedAddress; base: Wei; }>; 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 b79561794..3c037edda 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts @@ -1,8 +1,8 @@ +import { decodeReferrer } from "@namehash/ens-referrals"; import { makeSubdomainNode } from "enssdk"; import { addPrices, - decodeEncodedReferrer, PluginName, priceEth, type RegistrarActionPricingAvailable, @@ -262,7 +262,7 @@ export default function () { * emits a referrer in events. */ const encodedReferrer = event.args.referrer; - const decodedReferrer = decodeEncodedReferrer(encodedReferrer); + const decodedReferrer = decodeReferrer(encodedReferrer); const referral = { encodedReferrer, @@ -314,7 +314,7 @@ export default function () { * emits a referrer in events. */ const encodedReferrer = event.args.referrer; - const decodedReferrer = decodeEncodedReferrer(encodedReferrer); + const decodedReferrer = decodeReferrer(encodedReferrer); const referral = { encodedReferrer, diff --git a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts index 5bd158e75..9d293cb8a 100644 --- a/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts +++ b/apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts @@ -1,10 +1,7 @@ +import { decodeReferrer } from "@namehash/ens-referrals"; import { makeSubdomainNode } from "enssdk"; -import { - decodeEncodedReferrer, - PluginName, - type RegistrarActionReferralAvailable, -} from "@ensnode/ensnode-sdk"; +import { PluginName, type RegistrarActionReferralAvailable } from "@ensnode/ensnode-sdk"; import { getThisAccountId } from "@/lib/get-this-account-id"; import { addOnchainEventListener } from "@/lib/indexing-engines/ponder"; @@ -36,7 +33,7 @@ export default function () { * emits a referrer in events. */ const encodedReferrer = event.args.referrer; - const decodedReferrer = decodeEncodedReferrer(encodedReferrer); + const decodedReferrer = decodeReferrer(encodedReferrer); const referral = { encodedReferrer, 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 d8e756e92..a967c401d 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,8 +1,7 @@ -import type { Node, NormalizedAddress } from "enssdk"; +import type { Node, NormalizedAddress, Referrer } from "enssdk"; import type { Hash } from "viem"; import { - type EncodedReferrer, isRegistrarActionPricingAvailable, isRegistrarActionReferralAvailable, type RegistrarActionPricing, @@ -98,7 +97,7 @@ export async function handleRegistrarControllerEvent( } // 4. Prepare referral info - let encodedReferrer: EncodedReferrer | null; + let encodedReferrer: Referrer | null; let decodedReferrer: NormalizedAddress | null; if (isRegistrarActionReferralAvailable(referral)) { diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/universal-registrar-renewal-with-referrer-events.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/universal-registrar-renewal-with-referrer-events.ts index 31c0b319c..7c13026da 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/universal-registrar-renewal-with-referrer-events.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/universal-registrar-renewal-with-referrer-events.ts @@ -1,8 +1,7 @@ -import type { Address, Node } from "enssdk"; +import type { Address, Node, Referrer } from "enssdk"; import type { Hash } from "viem"; import { - type EncodedReferrer, isRegistrarActionReferralAvailable, type RegistrarActionReferral, } from "@ensnode/ensnode-sdk"; @@ -78,7 +77,7 @@ export async function handleUniversalRegistrarRenewalEvent( } // 3. Prepare referral info - let encodedReferrer: EncodedReferrer | null; + let encodedReferrer: Referrer | null; let decodedReferrer: Address | null; if (isRegistrarActionReferralAvailable(referral)) { From 5fdd5aadad133afa56e2bc524dbbb84638732888 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 21 Apr 2026 17:25:29 +0200 Subject: [PATCH 07/12] ENSApi: import `ZERO_ENCODED_REFERRER` from `@namehash/ens-referrals` Replaces the ENSNode SDK import --- apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts b/apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts index 73d8b5689..670cb3d11 100644 --- a/apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts +++ b/apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts @@ -1,3 +1,4 @@ +import { ZERO_ENCODED_REFERRER } from "@namehash/ens-referrals"; import { trace } from "@opentelemetry/api"; import { and, count, desc, eq, gte, isNotNull, lte, not, type SQL } from "drizzle-orm/sql"; import { asInterpretedName } from "enssdk"; @@ -18,7 +19,6 @@ import { type RegistrarActionsOrder, RegistrarActionsOrders, type RegistrationLifecycle, - ZERO_ENCODED_REFERRER, } from "@ensnode/ensnode-sdk"; import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; From ee442830fc8e863ef33e5ea14094b4a2241c641a Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 21 Apr 2026 17:25:43 +0200 Subject: [PATCH 08/12] Update lockfile --- pnpm-lock.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0caa9006c..5d8933c5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -511,6 +511,9 @@ importers: '@ensnode/ponder-sdk': specifier: workspace:* version: link:../../packages/ponder-sdk + '@namehash/ens-referrals': + specifier: workspace:* + version: link:../../packages/ens-referrals '@ponder/client': specifier: 'catalog:' version: 0.16.6(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) @@ -1165,6 +1168,9 @@ importers: '@ensnode/ensnode-sdk': specifier: workspace:* version: link:../ensnode-sdk + '@namehash/ens-referrals': + specifier: workspace:* + version: link:../ens-referrals '@radix-ui/react-avatar': specifier: ^1.1.10 version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) From d6938aab670c45062e2b73d15e0adad91975fbdb Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 21 Apr 2026 17:26:22 +0200 Subject: [PATCH 09/12] Fix `item is possibly undefined.` TS error --- packages/ens-referrals/src/api/zod-schemas.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ens-referrals/src/api/zod-schemas.ts b/packages/ens-referrals/src/api/zod-schemas.ts index 3ec30990a..000a9068f 100644 --- a/packages/ens-referrals/src/api/zod-schemas.ts +++ b/packages/ens-referrals/src/api/zod-schemas.ts @@ -339,9 +339,7 @@ export const makeReferralProgramEditionConfigSetArraySchema = ( return z.array(looseItemSchema).transform((items, ctx): ReferralProgramEditionConfig[] => { const result: ReferralProgramEditionConfig[] = []; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - + for (const [i, item] of items.entries()) { if (knownAwardModels.includes(item.rules.awardModel)) { // Known award model — fully validate. const parsed = configSchema.safeParse(item); From faa685d1bcd1c5db30857fbc2b3e9a9ea6828fb7 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 21 Apr 2026 17:27:05 +0200 Subject: [PATCH 10/12] =?UTF-8?q?docs(changeset):=20Added=20`Referrer`=20t?= =?UTF-8?q?ype=20to=20`enssdk`=20(raw=2032-byte=20onchain=20referrer=20byt?= =?UTF-8?q?es).=20Runtime=20helpers=20(`buildEncodedReferrer`,=20`decodeRe?= =?UTF-8?q?ferrer`=20=E2=80=94=20renamed=20from=20`decodeEncodedReferrer`,?= =?UTF-8?q?=20`ZERO=5FENCODED=5FREFERRER`,=20and=20related=20constants)=20?= =?UTF-8?q?moved=20from=20`@ensnode/ensnode-sdk`=20to=20`@namehash/ens-ref?= =?UTF-8?q?errals`,=20which=20now=20owns=20a=20branded=20`EncodedReferrer`?= =?UTF-8?q?=20type=20returned=20by=20`buildEncodedReferrer`.=20`buildEncod?= =?UTF-8?q?edReferrer`=20now=20accepts=20`Address`=20(previously=20`Normal?= =?UTF-8?q?izedAddress`)=20and=20normalizes=20internally.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/dirty-swans-arrive.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/dirty-swans-arrive.md diff --git a/.changeset/dirty-swans-arrive.md b/.changeset/dirty-swans-arrive.md new file mode 100644 index 000000000..39b2abc67 --- /dev/null +++ b/.changeset/dirty-swans-arrive.md @@ -0,0 +1,7 @@ +--- +"@namehash/ens-referrals": minor +"@ensnode/ensnode-sdk": minor +"enssdk": minor +--- + +Added `Referrer` type to `enssdk` (raw 32-byte onchain referrer bytes). Runtime helpers (`buildEncodedReferrer`, `decodeReferrer` — renamed from `decodeEncodedReferrer`, `ZERO_ENCODED_REFERRER`, and related constants) moved from `@ensnode/ensnode-sdk` to `@namehash/ens-referrals`, which now owns a branded `EncodedReferrer` type returned by `buildEncodedReferrer`. `buildEncodedReferrer` now accepts `Address` (previously `NormalizedAddress`) and normalizes internally. From 425d2b3004a2618d3167e61ef98a86e520f499c9 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 21 Apr 2026 18:43:17 +0200 Subject: [PATCH 11/12] Apply AI PR feedback --- packages/ens-referrals/src/encoded-referrer.ts | 8 ++++---- packages/ensnode-sdk/src/registrars/zod-schemas.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/ens-referrals/src/encoded-referrer.ts b/packages/ens-referrals/src/encoded-referrer.ts index 53f3b34e6..2fc8d90c4 100644 --- a/packages/ens-referrals/src/encoded-referrer.ts +++ b/packages/ens-referrals/src/encoded-referrer.ts @@ -17,19 +17,19 @@ export type EncodedReferrer = Referrer & { readonly __brand: "EncodedReferrer" } /** * Encoded Referrer byte offset * - * The count of left-padded bytes in an {@link Referrer} value. + * The count of left-padded bytes in an {@link EncodedReferrer} value. */ export const ENCODED_REFERRER_BYTE_OFFSET = 12; /** * Encoded Referrer byte length * - * The count of bytes the {@link Referrer} value consists of. + * The count of bytes the {@link EncodedReferrer} value consists of. */ export const ENCODED_REFERRER_BYTE_LENGTH = 32; /** - * Expected padding for a valid encoded referrer + * Expected padding for a valid {@link EncodedReferrer} * * Properly encoded referrers must have exactly 12 zero bytes of left padding * before the 20-byte Ethereum address. @@ -65,7 +65,7 @@ export function buildEncodedReferrer(address: Address): EncodedReferrer { } /** - * Decode an {@link Referrer} value into a {@link NormalizedAddress} + * Decode a {@link Referrer} value into a {@link NormalizedAddress} * according to the referrer encoding with left-zero-padding. * * @param referrer - The "raw" {@link Referrer} value to decode. diff --git a/packages/ensnode-sdk/src/registrars/zod-schemas.ts b/packages/ensnode-sdk/src/registrars/zod-schemas.ts index f9ceacbcd..f00831694 100644 --- a/packages/ensnode-sdk/src/registrars/zod-schemas.ts +++ b/packages/ensnode-sdk/src/registrars/zod-schemas.ts @@ -32,14 +32,14 @@ import { Subregistry } from "./subregistry"; * * The count of left-padded bytes in an {@link Referrer} value. */ -export const ENCODED_REFERRER_BYTE_OFFSET = 12; +const ENCODED_REFERRER_BYTE_OFFSET = 12; /** * Encoded Referrer byte length * * The count of bytes the {@link Referrer} value consists of. */ -export const ENCODED_REFERRER_BYTE_LENGTH = 32; +const ENCODED_REFERRER_BYTE_LENGTH = 32; /** * Expected padding for a valid encoded referrer @@ -47,13 +47,13 @@ export const ENCODED_REFERRER_BYTE_LENGTH = 32; * Properly encoded referrers must have exactly 12 zero bytes of left padding * before the 20-byte Ethereum address. */ -export const EXPECTED_ENCODED_REFERRER_PADDING: Hex = pad("0x", { +const EXPECTED_ENCODED_REFERRER_PADDING: Hex = pad("0x", { size: ENCODED_REFERRER_BYTE_OFFSET, dir: "left", }); /** - * Decode an {@link Referrer} value into a {@link NormalizedAddress} + * Decode a {@link Referrer} value into a {@link NormalizedAddress} * according to the referrer encoding with left-zero-padding. * * @param referrer - The "raw" {@link Referrer} value to decode. From 1c645e4f9fe9ac91899b724e8a71509f873fce06 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 22 Apr 2026 07:46:04 +0200 Subject: [PATCH 12/12] Apply AI PR feedback --- .../lib/universal-registrar-renewal-with-referrer-events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ensindexer/src/plugins/registrars/shared/lib/universal-registrar-renewal-with-referrer-events.ts b/apps/ensindexer/src/plugins/registrars/shared/lib/universal-registrar-renewal-with-referrer-events.ts index 7c13026da..ac8e52c5a 100644 --- a/apps/ensindexer/src/plugins/registrars/shared/lib/universal-registrar-renewal-with-referrer-events.ts +++ b/apps/ensindexer/src/plugins/registrars/shared/lib/universal-registrar-renewal-with-referrer-events.ts @@ -1,4 +1,4 @@ -import type { Address, Node, Referrer } from "enssdk"; +import type { Node, NormalizedAddress, Referrer } from "enssdk"; import type { Hash } from "viem"; import { @@ -78,7 +78,7 @@ export async function handleUniversalRegistrarRenewalEvent( // 3. Prepare referral info let encodedReferrer: Referrer | null; - let decodedReferrer: Address | null; + let decodedReferrer: NormalizedAddress | null; if (isRegistrarActionReferralAvailable(referral)) { encodedReferrer = referral.encodedReferrer;