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. 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"; 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..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,8 +1,7 @@ -import type { Address, Node } from "enssdk"; +import type { Node, NormalizedAddress, Referrer } from "enssdk"; import type { Hash } from "viem"; import { - type EncodedReferrer, isRegistrarActionReferralAvailable, type RegistrarActionReferral, } from "@ensnode/ensnode-sdk"; @@ -78,8 +77,8 @@ export async function handleUniversalRegistrarRenewalEvent( } // 3. Prepare referral info - let encodedReferrer: EncodedReferrer | null; - let decodedReferrer: Address | null; + let encodedReferrer: Referrer | null; + let decodedReferrer: NormalizedAddress | null; if (isRegistrarActionReferralAvailable(referral)) { encodedReferrer = referral.encodedReferrer; diff --git a/packages/ens-referrals/README.md b/packages/ens-referrals/README.md index 8a2a3111a..a8297e59b 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/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); 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 54% rename from packages/ensnode-sdk/src/registrars/encoded-referrer.ts rename to packages/ens-referrals/src/encoded-referrer.ts index 0943833f3..2fc8d90c4 100644 --- a/packages/ensnode-sdk/src/registrars/encoded-referrer.ts +++ b/packages/ens-referrals/src/encoded-referrer.ts @@ -1,17 +1,18 @@ -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 @@ -28,7 +29,7 @@ export const ENCODED_REFERRER_BYTE_OFFSET = 12; 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. @@ -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 a {@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/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