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

Type

Projects

Status

No status

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions