Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ensapi-beautified-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensapi": minor
---

**Omnigraph**: add `BeautifiedName` and `BeautifiedLabel` scalars, a `CanonicalName.beautified: BeautifiedName!` field, and a `Label.beautified: BeautifiedLabel!` field. These expose the Name/Label beautified per [ENSIP-15](https://docs.ens.domains/ensip/15) for display — continue using `interpreted` for navigation targets and lookup keys.
5 changes: 5 additions & 0 deletions .changeset/enssdk-beautify-interpreted-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"enssdk": minor
---

Add `beautifyInterpretedLabel`, which beautifies a single `InterpretedLabel` per [ENSIP-15](https://docs.ens.domains/ensip/15), preserving Encoded LabelHashes verbatim, and returns the new `BeautifiedLabel` branded type. `beautifyInterpretedName` is now defined in terms of `beautifyInterpretedLabel`.
4 changes: 4 additions & 0 deletions apps/ensapi/src/omnigraph-api/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import TracingPlugin, { isRootField } from "@pothos/plugin-tracing";
import ZodPlugin from "@pothos/plugin-zod";
import { AttributeNames, createOpenTelemetryWrapper } from "@pothos/tracing-opentelemetry";
import type {
BeautifiedLabel,
BeautifiedName,
ChainId,
CoinType,
DomainId,
Expand Down Expand Up @@ -66,6 +68,8 @@ export type BuilderScalars = {
Node: { Input: Node; Output: Node };
InterpretedName: { Input: InterpretedName; Output: InterpretedName };
InterpretedLabel: { Input: InterpretedLabel; Output: InterpretedLabel };
BeautifiedName: { Input: BeautifiedName; Output: BeautifiedName };
BeautifiedLabel: { Input: BeautifiedLabel; Output: BeautifiedLabel };
DomainId: { Input: DomainId; Output: DomainId };
RegistryId: { Input: RegistryId; Output: RegistryId };
ResolverId: { Input: ResolverId; Output: ResolverId };
Expand Down
17 changes: 17 additions & 0 deletions apps/ensapi/src/omnigraph-api/schema/canonical-name.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { beautifyInterpretedName } from "enssdk";

import { builder } from "@/omnigraph-api/builder";
import type { Domain } from "@/omnigraph-api/schema/domain";

Expand All @@ -24,5 +26,20 @@ CanonicalNameRef.implement({
return domain.canonicalName;
},
}),
beautified: t.field({
description:
"The Canonical Name as a BeautifiedName: the InterpretedName with its normalized labels beautified per ENSIP-15 (https://docs.ens.domains/ensip/15) for display. Encoded LabelHash labels are preserved verbatim. Display-only; use `interpreted` for navigation targets and lookup keys.",
type: "BeautifiedName",
nullable: false,
resolve: (domain) => {
if (!domain.canonicalName) {
throw new Error(
`Invariant(CanonicalName.beautified): canonical Domain '${domain.id}' is missing canonicalName.`,
);
}

return beautifyInterpretedName(domain.canonicalName);
},
}),
}),
});
13 changes: 13 additions & 0 deletions apps/ensapi/src/omnigraph-api/schema/label.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { beautifyInterpretedLabel } from "enssdk";

import type { ensIndexerSchema } from "@/lib/ensdb/singleton";
import { builder } from "@/omnigraph-api/builder";

Expand Down Expand Up @@ -26,5 +28,16 @@ LabelRef.implement({
nullable: false,
resolve: (parent) => parent.interpreted,
}),

///////////////////
// Label.beautified
///////////////////
beautified: t.field({
description:
"The Label as a BeautifiedLabel: the Interpreted Label beautified per ENSIP-15 (https://docs.ens.domains/ensip/15) for display. An Encoded LabelHash is preserved verbatim. Display-only; use `interpreted` for lookup keys. \n(@see https://ensnode.io/docs/reference/terminology#interpreted-label)",
Comment thread
shrugs marked this conversation as resolved.
type: "BeautifiedLabel",
nullable: false,
resolve: (parent) => beautifyInterpretedLabel(parent.interpreted),
}),
}),
});
52 changes: 38 additions & 14 deletions apps/ensapi/src/omnigraph-api/schema/scalars.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
asInterpretedLabel,
asInterpretedName,
type BeautifiedLabel,
type BeautifiedName,
type ChainId,
type CoinType,
type DomainId,
Expand Down Expand Up @@ -62,19 +64,19 @@ builder.scalarType("Hex", {
});

builder.scalarType("ChainId", {
description: "ChainId represents a enssdk#ChainId.",
description: "ChainId represents an enssdk#ChainId.",
serialize: (value: ChainId) => value,
parseValue: (value) => makeChainIdSchema("ChainId").parse(value),
});

builder.scalarType("CoinType", {
description: "CoinType represents a enssdk#CoinType.",
description: "CoinType represents an enssdk#CoinType.",
serialize: (value: CoinType) => value,
parseValue: (value) => makeCoinTypeSchema("CoinType").parse(value),
});

builder.scalarType("Node", {
description: "Node represents a enssdk#Node.",
description: "Node represents an enssdk#Node.",
serialize: (value: Node) => value,
parseValue: (value) =>
z.coerce
Expand All @@ -93,7 +95,7 @@ builder.scalarType("Node", {
});

builder.scalarType("InterpretedName", {
description: "InterpretedName represents a enssdk#InterpretedName.",
description: "InterpretedName represents an enssdk#InterpretedName.",
serialize: (value: Name) => value,
parseValue: (value) =>
z.coerce
Expand All @@ -113,7 +115,7 @@ builder.scalarType("InterpretedName", {
});

builder.scalarType("InterpretedLabel", {
description: "InterpretedLabel represents a enssdk#InterpretedLabel.",
description: "InterpretedLabel represents an enssdk#InterpretedLabel.",
serialize: (value: Name) => value,
parseValue: (value) =>
z.coerce
Expand All @@ -131,8 +133,30 @@ builder.scalarType("InterpretedLabel", {
.parse(value),
});

builder.scalarType("BeautifiedName", {
description:
"BeautifiedName represents an enssdk#BeautifiedName: an InterpretedName whose normalized labels have been beautified per ENSIP-15 (https://docs.ens.domains/ensip/15) for display. It is display-only and MUST NOT be used as a navigation target or lookup key.",
serialize: (value: BeautifiedName) => value,
Comment thread
shrugs marked this conversation as resolved.
parseValue: (value) =>
z.coerce
.string()
.transform((val) => val as BeautifiedName)
.parse(value),
});

builder.scalarType("BeautifiedLabel", {
description:
"BeautifiedLabel represents an enssdk#BeautifiedLabel: an InterpretedLabel beautified per ENSIP-15 (https://docs.ens.domains/ensip/15) for display. It is display-only and MUST NOT be used as a lookup key.",
serialize: (value: BeautifiedLabel) => value,
parseValue: (value) =>
z.coerce
.string()
.transform((val) => val as BeautifiedLabel)
.parse(value),
});

builder.scalarType("DomainId", {
description: "DomainId represents a enssdk#DomainId.",
description: "DomainId represents an enssdk#DomainId.",
serialize: (value: DomainId) => value,
parseValue: (value) =>
z.coerce
Expand All @@ -142,7 +166,7 @@ builder.scalarType("DomainId", {
});

builder.scalarType("RegistryId", {
description: "RegistryId represents a enssdk#RegistryId.",
description: "RegistryId represents an enssdk#RegistryId.",
serialize: (value: RegistryId) => value,
parseValue: (value) =>
z.coerce
Expand All @@ -152,7 +176,7 @@ builder.scalarType("RegistryId", {
});

builder.scalarType("ResolverId", {
description: "ResolverId represents a enssdk#ResolverId.",
description: "ResolverId represents an enssdk#ResolverId.",
serialize: (value: ResolverId) => value,
parseValue: (value) =>
z.coerce
Expand All @@ -162,7 +186,7 @@ builder.scalarType("ResolverId", {
});

builder.scalarType("PermissionsId", {
description: "PermissionsId represents a enssdk#PermissionsId.",
description: "PermissionsId represents an enssdk#PermissionsId.",
serialize: (value: PermissionsId) => value,
parseValue: (value) =>
z.coerce
Expand All @@ -172,7 +196,7 @@ builder.scalarType("PermissionsId", {
});

builder.scalarType("PermissionsResourceId", {
description: "PermissionsResourceId represents a enssdk#PermissionsResourceId.",
description: "PermissionsResourceId represents an enssdk#PermissionsResourceId.",
serialize: (value: PermissionsResourceId) => value,
parseValue: (value) =>
z.coerce
Expand All @@ -182,7 +206,7 @@ builder.scalarType("PermissionsResourceId", {
});

builder.scalarType("PermissionsUserId", {
description: "PermissionsUserId represents a enssdk#PermissionsUserId.",
description: "PermissionsUserId represents an enssdk#PermissionsUserId.",
serialize: (value: PermissionsUserId) => value,
parseValue: (value) =>
z.coerce
Expand All @@ -192,7 +216,7 @@ builder.scalarType("PermissionsUserId", {
});

builder.scalarType("RegistrationId", {
description: "RegistrationId represents a enssdk#RegistrationId.",
description: "RegistrationId represents an enssdk#RegistrationId.",
serialize: (value: RegistrationId) => value,
parseValue: (value) =>
z.coerce
Expand All @@ -202,7 +226,7 @@ builder.scalarType("RegistrationId", {
});

builder.scalarType("RenewalId", {
description: "RenewalId represents a enssdk#RenewalId.",
description: "RenewalId represents an enssdk#RenewalId.",
serialize: (value: RenewalId) => value,
parseValue: (value) =>
z.coerce
Expand All @@ -212,7 +236,7 @@ builder.scalarType("RenewalId", {
});

builder.scalarType("ResolverRecordsId", {
description: "ResolverRecordsId represents a enssdk#ResolverRecordsId.",
description: "ResolverRecordsId represents an enssdk#ResolverRecordsId.",
serialize: (value: ResolverRecordsId) => value,
parseValue: (value) =>
z.coerce
Expand Down
15 changes: 14 additions & 1 deletion packages/enssdk/src/lib/beautify.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { describe, expect, it } from "vitest";

import { beautifyInterpretedName } from "./beautify";
import { beautifyInterpretedLabel, beautifyInterpretedName } from "./beautify";
import { ENS_ROOT_NAME } from "./constants";
import { asInterpretedName } from "./interpreted-names-and-labels";
import type { InterpretedLabel } from "./types";

describe("beautifyInterpretedLabel", () => {
it("beautifies a normalized label", () => {
expect(beautifyInterpretedLabel("♾♾♾♾" as InterpretedLabel)).toEqual("♾️♾️♾️♾️");
});

it("preserves an Encoded LabelHash label verbatim", () => {
const label =
"[0000000000000000000000000000000000000000000000000000000000000001]" as InterpretedLabel;
expect(beautifyInterpretedLabel(label)).toEqual(label);
});
});

describe("beautifyInterpretedName", () => {
it("returns the ENS Root Name unchanged", () => {
Expand Down
31 changes: 21 additions & 10 deletions packages/enssdk/src/lib/beautify.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
import { ens_beautify } from "@adraffy/ens-normalize";

import { ENS_ROOT_NAME } from "./constants";
import { interpretedNameToInterpretedLabels } from "./interpreted-names-and-labels";
import { isEncodedLabelHash } from "./labelhash";
import type { BeautifiedName, InterpretedName } from "./types";
import type { BeautifiedLabel, BeautifiedName, InterpretedLabel, InterpretedName } from "./types";

/**
* Converts an {@link InterpretedName} into a {@link BeautifiedName} suitable for presentation in a UI.
* Converts an {@link InterpretedLabel} into a {@link BeautifiedLabel} suitable for presentation in a UI.
*
* Each label of the InterpretedName is either an Encoded LabelHash or a normalized Label:
* - Encoded LabelHash labels are preserved verbatim.
* - Normalized Labels are passed through {@link ens_beautify}, producing a Label that is
* normalizable (and normalizes back to the input) but may itself be unnormalized.
*
* The resulting BeautifiedLabel is suitable for display but is NOT an InterpretedLabel, and the
* branded return type prevents it from being passed to APIs that expect one. Continue to use the
* source InterpretedLabel for lookup keys and anywhere else that expects an InterpretedLabel.
*
* @example
* ```ts
* beautifyInterpretedLabel("♾♾♾♾" as InterpretedLabel) // → "♾️♾️♾️♾️"
* ```
*/
export const beautifyInterpretedLabel = (label: InterpretedLabel): BeautifiedLabel =>
(isEncodedLabelHash(label) ? label : ens_beautify(label)) as BeautifiedLabel;

/**
* Converts an {@link InterpretedName} into a {@link BeautifiedName} suitable for presentation in a UI
* by beautifying each of its labels via {@link beautifyInterpretedLabel}.
*
* The resulting BeautifiedName is suitable for display but is NOT an InterpretedName, and the
* branded return type prevents it from being passed to APIs that expect one. Continue to use the
* source InterpretedName for navigation targets, lookup keys, and anywhere else that expects an
Expand All @@ -23,10 +37,7 @@ import type { BeautifiedName, InterpretedName } from "./types";
* beautifyInterpretedName("♾♾♾♾.eth" as InterpretedName) // → "♾️♾️♾️♾️.eth"
* ```
*/
export const beautifyInterpretedName = (name: InterpretedName): BeautifiedName => {
if (name === ENS_ROOT_NAME) return name as string as BeautifiedName;

return interpretedNameToInterpretedLabels(name)
.map((label) => (isEncodedLabelHash(label) ? label : ens_beautify(label)))
export const beautifyInterpretedName = (name: InterpretedName): BeautifiedName =>
interpretedNameToInterpretedLabels(name)
.map(beautifyInterpretedLabel)
.join(".") as BeautifiedName;
};
18 changes: 18 additions & 0 deletions packages/enssdk/src/lib/types/ens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,23 @@ export type LiteralLabel = Label & { __brand: "LiteralLabel" };
*/
export type InterpretedLabel = Label & { __brand: "InterpretedLabel" };

/**
* A Beautified Label is a Label produced for presentation in a UI from an {@link InterpretedLabel}.
*
* It is either:
* a) an Encoded LabelHash, preserved verbatim from the source InterpretedLabel, or
* b) a Label produced by passing a normalized Label through ENSIP-15 beautification, which is
* guaranteed to be normalizable back to the original normalized Label but is itself NOT
* necessarily normalized (e.g. `"♾"` → `"♾️"`).
*
* Because (b) is not guaranteed to be normalized, a BeautifiedLabel is NOT an InterpretedLabel and
* MUST NOT be used as a lookup key or anywhere else that expects an InterpretedLabel.
*
* @see https://docs.ens.domains/ensip/15
* @dev nominally typed to enforce usage & enhance codebase clarity
*/
export type BeautifiedLabel = Label & { __brand: "BeautifiedLabel" };

/**
* A Literal Name is a Name as it literally appears onchain, composed of 0 or more Literal Labels
* joined by dots. It may be an unnormalized name for reasons including:
Expand Down Expand Up @@ -154,6 +171,7 @@ export type InterpretedName = Name & { __brand: "InterpretedName" };
* MUST NOT be used as a navigation target, lookup key, or anywhere else that expects an
* InterpretedName.
*
* @see https://docs.ens.domains/ensip/15
* @dev nominally typed to enforce usage & enhance codebase clarity
*/
export type BeautifiedName = Name & { __brand: "BeautifiedName" };
Expand Down
32 changes: 32 additions & 0 deletions packages/enssdk/src/omnigraph/generated/introspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,14 @@ const introspection = {
}
]
},
{
"kind": "SCALAR",
"name": "BeautifiedLabel"
},
{
"kind": "SCALAR",
"name": "BeautifiedName"
},
{
"kind": "SCALAR",
"name": "BigInt"
Expand All @@ -1041,6 +1049,18 @@ const introspection = {
"kind": "OBJECT",
"name": "CanonicalName",
"fields": [
{
"name": "beautified",
"type": {
"kind": "NON_NULL",
"ofType": {
"kind": "SCALAR",
"name": "BeautifiedName"
}
},
"args": [],
"isDeprecated": false
},
{
"name": "interpreted",
"type": {
Expand Down Expand Up @@ -3571,6 +3591,18 @@ const introspection = {
"kind": "OBJECT",
"name": "Label",
"fields": [
{
"name": "beautified",
"type": {
"kind": "NON_NULL",
"ofType": {
"kind": "SCALAR",
"name": "BeautifiedLabel"
}
},
"args": [],
"isDeprecated": false
},
{
"name": "hash",
"type": {
Expand Down
Loading
Loading