Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: include domain logics into ens-utils #341

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
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/neat-lamps-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@namehash/ens-utils": minor
---

Add domain related logics to ens-utils (Registration, DomainName, DomainCard, etc.)
1 change: 1 addition & 0 deletions packages/ens-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"viem": "2.9.3"
},
"devDependencies": {
"@types/node": "22.0.0",
"tsup": "8.0.2",
"typescript": "5.3.3",
"vite": "5.1.7",
Expand Down
151 changes: 151 additions & 0 deletions packages/ens-utils/src/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { NFTRef } from "./nft";
import { ENSName } from "./ensname";
import { Address, isAddressEqual } from "./address";
import { keccak256, labelhash as labelHash } from "viem";
import { Registration } from "./ethregistrar";

export interface DomainCard {
name: ENSName;

/**
* A reference to the NFT associated with `name`.
*
* null if and only if one or more of the following are true:
* 1. name is not normalized
* 2. name is not currently minted (name is on primary market, not secondary market) and the name is not currently expired in grace period
* 3. we don't know a strategy to generate a NFTRef for the name on the specified chain (ex: name is associated with an unknown registrar)
*/
nft: NFTRef | null;
registration: Registration;
ownerAddress: Address | null;
managerAddress: Address | null;
/** Former owner address is only set when the domain is in Grace Period */
formerOwnerAddress: Address | null;
/** Former manager address is only set when the domain is in Grace Period */
formerManagerAddress: Address | null;
}

/* Defines the ownership of a domain for a given address */
export const UserOwnershipOfDomain = {
/* NoOwner: If domain has no owner */
NoOwner: "NoOwner",

/* NotOwner: If domain has an owner but user is not the owner */
NotOwner: "NotOwner",

/* FormerOwner: If user is owner of the domain and domain is in Grace Period */
FormerOwner: "FormerOwner",

/* ActiveOwner: If user is owner of the domain and domain is not in Grace Period */
ActiveOwner: "ActiveOwner",
};
export type UserOwnershipOfDomain =
(typeof UserOwnershipOfDomain)[keyof typeof UserOwnershipOfDomain];

/**
* Returns the ownership status of a domain in comparison to the current user's address
* @param domain Domain that is being checked. If null, returns UserOwnershipOfDomain.NoOwner
* @param currentUserAddress Address of the current user.
* If null, returns UserOwnershipOfDomain.NoOwner or UserOwnershipOfDomain.NotOwner
* @returns UserOwnershipOfDomain
*/
export const getCurrentUserOwnership = (
FrancoAguzzi marked this conversation as resolved.
Show resolved Hide resolved
domain: DomainCard | null,
FrancoAguzzi marked this conversation as resolved.
Show resolved Hide resolved
currentUserAddress: Address | null,
FrancoAguzzi marked this conversation as resolved.
Show resolved Hide resolved
): UserOwnershipOfDomain => {
const formerDomainOwnerAddress =
domain && domain.formerOwnerAddress ? domain.formerOwnerAddress : null;
const ownerAddress =
domain && domain.ownerAddress ? domain.ownerAddress : null;

if (currentUserAddress && formerDomainOwnerAddress) {
const isFormerOwner =
formerDomainOwnerAddress &&
isAddressEqual(formerDomainOwnerAddress, currentUserAddress);

if (isFormerOwner) {
return UserOwnershipOfDomain.FormerOwner;
}

const isOwner =
ownerAddress && isAddressEqual(currentUserAddress, ownerAddress);

if (isOwner) {
return UserOwnershipOfDomain.ActiveOwner;
}
}

if (!ownerAddress) {
return UserOwnershipOfDomain.NoOwner;
}

return UserOwnershipOfDomain.NotOwner;
};

export enum ParseNameErrorCode {
Empty = "Empty",
TooShort = "TooShort",
UnsupportedTLD = "UnsupportedTLD",
UnsupportedSubdomain = "UnsupportedSubdomain",
MalformedName = "MalformedName",
MalformedLabelHash = "MalformedLabelHash",
}

type ParseNameErrorDetails = {
normalizedName: string | null;
displayName: string | null;
};
export class ParseNameError extends Error {
public readonly errorCode: ParseNameErrorCode;
public readonly errorDetails: ParseNameErrorDetails | null;

constructor(
message: string,
errorCode: ParseNameErrorCode,
errorDetails: ParseNameErrorDetails | null,
) {
super(message);

this.errorCode = errorCode;
this.errorDetails = errorDetails;
}
}

export const DEFAULT_TLD = "eth";

export const DefaultParseNameError = new ParseNameError(
"Empty name",
ParseNameErrorCode.Empty,
null,
);

export const hasMissingNameFormat = (label: string) =>
new RegExp("\\[([0123456789abcdef]*)\\]").test(label) && label.length === 66;

const labelhash = (label: string) => labelHash(label);

const keccak = (input: Buffer | string) => {
let out = null;
if (Buffer.isBuffer(input)) {
out = keccak256(input);
} else {
out = labelhash(input);
}
return out.slice(2); // cut 0x
};

const initialNode =
"0000000000000000000000000000000000000000000000000000000000000000";

export const namehashFromMissingName = (inputName: string): string => {
let node = initialNode;

const split = inputName.split(".");
const labels = [split[0].slice(1, -1), keccak(split[1])];

for (let i = labels.length - 1; i >= 0; i--) {
const labelSha = labels[i];
node = keccak(Buffer.from(node + labelSha, "hex"));
}
return "0x" + node;
};
27 changes: 21 additions & 6 deletions packages/ens-utils/src/ensname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ export const MIN_ETH_REGISTRABLE_LABEL_LENGTH = 3;
*/
export const Normalization = {
/** `normalized`: The name or label is normalized. */
Normalized: 'normalized',
Normalized: "normalized",

/** `unnormalized`: The name or label is not normalized. */
Unnormalized: 'unnormalized',
Unnormalized: "unnormalized",

/** `unknown`: The name or label is unknown because it cannot be looked up from its hash. */
Unknown: 'unknown',
Unknown: "unknown",
} as const;

export type Normalization = (typeof Normalization)[keyof typeof Normalization];
Expand Down Expand Up @@ -83,6 +83,21 @@ export interface ENSName {
node: `0x${string}`;
}

export const getDomainLabelFromENSName = (ensName: ENSName): string | null => {
if (ensName.labels.length !== 2) return null;

if (ensName.labels[1] !== ETH_TLD) return null;

// NOTE: now we know we have a direct subname of ".eth"

const subnameLength = charCount(ensName.labels[0]);

// ensure this subname is even possible to register
if (subnameLength < MIN_ETH_REGISTRABLE_LABEL_LENGTH) return null;

return ensName.labels[0];
};

/**
* Compares two sets of labels for deep equality
* @param labels1 the first set of labels
Expand Down Expand Up @@ -266,7 +281,7 @@ export function getNamespaceRoot(name: ENSName): NamespaceRoot {
* `unknown` if the decentralization status of the name is unknown.
*/
export function getDecentralizationStatus(
name: ENSName
name: ENSName,
): DecentralizationStatus {
switch (getNamespaceRoot(name)) {
case "ens":
Expand Down Expand Up @@ -328,11 +343,11 @@ export function getRegistrationPotential(name: ENSName): RegistrationPotential {

/**
* Calculates the number of characters in a label.
*
*
* NOTE: This length will be the same as determined by the EthRegistrarController smart contracts.
* These contracts calculate length using the following code that counts Unicode characters in UTF-8 encoding.
* https://github.com/ensdomains/ens-contracts/blob/staging/contracts/ethregistrar/StringUtils.sol
*
*
* This length may be different than the traditional ".length" property of a string in JavaScript.
* In Javascript, the ".length" property of a string returns the number of UTF-16 code units in that string.
* UTF-16 represents Unicode characters with codepoints higher can fit within a 16 bit value as a "surrogate pair"
Expand Down
Loading
Loading