Skip to content

ens-referrals: move referrer encode/decode to ens-referrals #1927

@shrugs

Description

@shrugs

spec via #1908 (comment)


Move encoded-referrer helpers to @namehash/ens-referrals; hoist Referrer type to enssdk

Context

PR review flagged that buildEncodedReferrer / decodeEncodedReferrer live in @ensnode/ensnode-sdk but are really community-facing utilities for the ENS Referral Program — they belong in @namehash/ens-referrals. Also: buildEncodedReferrer should accept Address (not NormalizedAddress) and normalize internally via toNormalizedAddress, which already throws on invalid input.

Naive "move the file" creates a circular dep (ens-referrals already depends on ensnode-sdk, so ensnode-sdk can't import EncodedReferrer back). We sidestep it by hoisting the one-line type alias to enssdk, the leaf package both sides already depend on. The zod invariant check in ensnode-sdk that currently uses decodeEncodedReferrer inlines its ~10 lines of decode logic to avoid needing a runtime import from ens-referrals.

No dep inversion: ens-referrals → ensnode-sdk stays as today.

Design decisions

  • Raw type lives in enssdk, renamed EncodedReferrerReferrer (type Referrer = Hex). Unbranded, consistent with the NormalizedAddress policy. Represents raw 32-byte onchain referrer bytes — contracts may emit arbitrary bytes in that slot, so the type can't promise more than "hex".
  • Branded EncodedReferrer lives in ens-referrals as type EncodedReferrer = Referrer & { readonly __brand: "EncodedReferrer" }. Represents "a Referrer that is guaranteed to be validly encoded" — 32-byte hex with 12 bytes of zero padding followed by a 20-byte lowercase address. Constructible only through buildEncodedReferrer (and ZERO_ENCODED_REFERRER). The intersection means it trivially downcasts to Referrer/Hex/string when passed to viem APIs or contract calls.
  • Runtime helpers live in ens-referrals at packages/ens-referrals/src/encoded-referrer.ts (top-level, not under v1/ — useful across versions). Exported from packages/ens-referrals/src/index.ts only (NOT re-exported from src/v1/index.ts). Consumer import path is @namehash/ens-referrals.
  • ens-referrals does not re-export Referrer — consumers get it from enssdk directly.
  • buildEncodedReferrer(address: Address): EncodedReferrer — takes Address, normalizes via toNormalizedAddress (throws on invalid), pads to 32 bytes, returns the branded type (internal as EncodedReferrer assertion is the only cast).
  • decodeEncodedReferrer renamed to decodeReferrer; runtime behavior unchanged(referrer: Referrer): NormalizedAddress. Throws on wrong byte length, throws on invalid trailing address bytes, returns zeroAddress on malformed 12-byte padding. Only the name changes; callers keep their current patterns (no ?? zeroAddress coalesce needed). Accepts the unbranded Referrer because typical inputs come straight from protocol bytes; branded EncodedReferrer also works via subtyping.
  • Asymmetric pair buildEncodedReferrer + decodeReferrer by design.
  • Constants keep their names. ZERO_ENCODED_REFERRER is typed as EncodedReferrer (it is a valid encoding of the zero address). ENCODED_REFERRER_BYTE_LENGTH, ENCODED_REFERRER_BYTE_OFFSET, EXPECTED_ENCODED_REFERRER_PADDING describe the encoding format.
  • RegistrarAction.encodedReferrer field name stays (rename deferred to a follow-up). Its type stays Referrer (unbranded) since the stored value comes from raw protocol bytes and may not satisfy the branded invariant.

File changes

enssdk

  • packages/enssdk/src/lib/types/evm.ts — append export type Referrer = Hex; alongside Hex/Address/NormalizedAddress. Doc comment stays ENS-level: "raw 32-byte onchain referrer value as emitted by ENS registrar controllers". Verify the existing barrel chain (src/index.tssrc/lib/index.tssrc/lib/types/evm.ts) picks it up automatically.

ens-referrals — create runtime helpers

  • packages/ens-referrals/src/encoded-referrer.ts:
    • import type { Address, NormalizedAddress, Referrer } from "enssdk";
    • import { toNormalizedAddress } from "enssdk";
    • import { pad, size, slice, zeroAddress } from "viem";
    • export type EncodedReferrer = Referrer & { readonly __brand: "EncodedReferrer" }; — doc comment explains the invariant (32-byte, 12-byte zero padding, 20-byte lowercase address) and that the brand is only produced by the helpers in this module.
    • Constants: ENCODED_REFERRER_BYTE_OFFSET = 12, ENCODED_REFERRER_BYTE_LENGTH = 32, EXPECTED_ENCODED_REFERRER_PADDING = pad("0x", { size: 12, dir: "left" }), ZERO_ENCODED_REFERRER: EncodedReferrer = pad("0x", { size: 32, dir: "left" }) as EncodedReferrer.
    • export function buildEncodedReferrer(address: Address): EncodedReferrer { return pad(toNormalizedAddress(address), { size: ENCODED_REFERRER_BYTE_LENGTH, dir: "left" }) as EncodedReferrer; }
    • export function decodeReferrer(referrer: Referrer): NormalizedAddress — body ported verbatim from the current decodeEncodedReferrer: throws on wrong length, returns zeroAddress on malformed padding, throws when trailing bytes aren't a valid address. Parameter is unbranded Referrer; a branded EncodedReferrer upcasts to it via subtyping.
    • Do not re-export Referrer from this file. EncodedReferrer is exported (it's this module's own type).
  • packages/ens-referrals/src/encoded-referrer.test.ts — port the existing tests. Update the building encoded referrer block to pass Address directly (both lowercase and checksummed) without first casting to NormalizedAddress. Add a case for buildEncodedReferrer("0xnotavalidaddress" as Address) throwing from toNormalizedAddress. Rename internal references from decodeEncodedReferrer to decodeReferrer.
  • packages/ens-referrals/src/index.ts — add export * from "./encoded-referrer";. Do not add to src/v1/index.ts.
  • packages/ens-referrals/README.md — append a buildEncodedReferrer subsection to "Other Utilities" (currently lines 162-175). Import path in the example: @namehash/ens-referrals (root). Do not document decodeReferrer.

ensnode-sdk — delete the module, inline the zod check, fix the field type

  • Delete packages/ensnode-sdk/src/registrars/encoded-referrer.ts.
  • Delete packages/ensnode-sdk/src/registrars/encoded-referrer.test.ts.
  • packages/ensnode-sdk/src/registrars/index.ts — remove export * from "./encoded-referrer";.
  • packages/ensnode-sdk/src/registrars/registrar-action.ts:
    • Drop import type { EncodedReferrer } from "./encoded-referrer" (line 4) and the two re-export lines (lines 6, 7).
    • Add import type { Referrer } from "enssdk";.
    • Change RegistrarActionReferralAvailable.encodedReferrer: EncodedReferrer: Referrer (field name stays).
    • Change RegistrarActionReferralAvailable.decodedReferrer: NormalizedAddress (field name stays)
  • packages/ensnode-sdk/src/registrars/zod-schemas.ts:
    • Drop import { decodeEncodedReferrer, ENCODED_REFERRER_BYTE_LENGTH } from "./encoded-referrer".
    • Add local const ENCODED_REFERRER_BYTE_LENGTH = 32; and const ENCODED_REFERRER_BYTE_OFFSET = 12;.
    • In invariant_registrarActionDecodedReferrerBasedOnRawReferrer (lines 89-116), inline the decode: check 32-byte size, slice the 12-byte padding, compare to the zero-padded constant, slice the trailing 20 bytes, coerce via toNormalizedAddress. Match the current throw-then-custom-issue pattern. Use size/slice/zeroAddress from viem and toNormalizedAddress from enssdk.

Consumer import updates

Referrer type imports — swap import { type EncodedReferrer, ... } from "@ensnode/ensnode-sdk" to import type { Referrer } from "enssdk" (other symbols on the same import line remain from @ensnode/ensnode-sdk):

  • apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts — lines 13 import, 64 referrer: EncodedReferrer, 141 same.
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/RegistrarController.ts — line 12 import, 41 referrer?: EncodedReferrer, 94 same.
  • apps/ensindexer/src/plugins/registrars/shared/lib/registrar-controller-events.ts — import + let encodedReferrer: EncodedReferrer | null;Referrer | null (variable name preserved).
  • apps/ensindexer/src/plugins/registrars/shared/lib/universal-registrar-renewal-with-referrer-events.ts — same.
  • packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts — line 20 import; $type<EncodedReferrer>()$type<Referrer>() at lines 364 and 446.
  • packages/ensdb-sdk/src/ensindexer-abstract/registrars.schema.ts — any $type<EncodedReferrer>() with matching import swap.

Runtime helper imports — swap from @ensnode/ensnode-sdk to @namehash/ens-referrals, and rename decodeEncodedReferrerdecodeReferrer:

  • apps/ensapi/src/lib/registrar-actions/find-registrar-actions.tsZERO_ENCODED_REFERRER. (ensapi already depends on @namehash/ens-referrals.)
  • apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_UniversalRegistrarRenewalWithReferrer.ts — rename decodeEncodedReferrer(...)decodeReferrer(...) at line 39; update import.
  • apps/ensindexer/src/plugins/registrars/ethnames/handlers/Ethnames_RegistrarController.ts — same at lines 265 and 317.
  • packages/namehash-ui/src/components/registrar-actions/RegistrarActionCard.tsxZERO_ENCODED_REFERRER.

Package dependency additions (workspace:*)

  • apps/ensindexer/package.json — add @namehash/ens-referrals.
  • packages/namehash-ui/package.json — add @namehash/ens-referrals.
  • packages/ensdb-sdk/package.json — no new dep (Referrer comes from enssdk, already a dep).

Changeset

.changeset/<slug>.md:

---
"enssdk": minor
"@namehash/ens-referrals": minor
"@ensnode/ensnode-sdk": 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.

Fixed-group version bumps cascade to the other workspace packages automatically.

Critical files to read before editing

  • packages/ensnode-sdk/src/registrars/encoded-referrer.ts — source being moved.
  • packages/ensnode-sdk/src/registrars/encoded-referrer.test.ts — tests being ported.
  • packages/ensnode-sdk/src/registrars/zod-schemas.ts:89-116 — invariant check to rewrite inline.
  • packages/ensnode-sdk/src/registrars/registrar-action.ts:1-7, 110-130 — imports and RegistrarActionReferralAvailable field.
  • packages/enssdk/src/lib/types/evm.ts — destination for Referrer type.
  • packages/enssdk/src/index.ts / src/lib/index.ts — barrel chain verification.
  • packages/ens-referrals/src/index.ts, README.md:162-175.
  • packages/enssdk/src/lib/address.ts — confirm toNormalizedAddress throws on invalid input (it does).

Reuse

  • toNormalizedAddress (enssdk) — inside buildEncodedReferrer and the inlined zod invariant.
  • pad, size, slice, zeroAddress (viem) — unchanged from the current implementation.

Verification

  1. pnpm install — confirm new workspace deps resolve.
  2. Typecheck in parallel:
    • pnpm -F enssdk typecheck
    • pnpm -F @namehash/ens-referrals typecheck
    • pnpm -F @ensnode/ensnode-sdk typecheck
    • pnpm -F @ensnode/ensdb-sdk typecheck
    • pnpm -F ensindexer typecheck
    • pnpm -F ensapi typecheck
    • pnpm -F @namehash/namehash-ui typecheck
  3. pnpm lint
  4. Tests:
    • pnpm test --project ens-referrals — ported + new buildEncodedReferrer(Address) / invalid-input tests pass.
    • pnpm test --project ensnode-sdk — registrar-action zod invariant still validates after inlining.
    • pnpm test --project ensindexer — registrar event handler regressions.
  5. Grep for lingering EncodedReferrer identifier and lingering decodeEncodedReferrer reference — should return zero hits.

Metadata

Metadata

Assignees

Labels

devopsDevOps related
No fields configured for Feature.

Projects

Status

In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions