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

Refine domain logic #343

Open
wants to merge 29 commits into
base: francoaguzzi/sc-25546/include-domain-logics-into-ens-utils
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bb158ff
Refine changeset
lightwalker-eth Aug 2, 2024
da47242
Refine docs for UserOwnershipOfDomain
lightwalker-eth Aug 2, 2024
c0b096a
Refine docs for getCurrentUserOwnership
lightwalker-eth Aug 2, 2024
1523847
Refine getCurrentUserOwnership
lightwalker-eth Aug 2, 2024
31c4695
Refine getCurrentUserOwnership
lightwalker-eth Aug 2, 2024
fb2897e
Move logic from domain.ts that belongs in ensname.ts
lightwalker-eth Aug 2, 2024
a57c826
Move function out of ensname to ethregistrar where it belongs
lightwalker-eth Aug 2, 2024
f3bebbc
Refine basePrice related logic for Eth2LD names
lightwalker-eth Aug 2, 2024
eec5b92
Move ideas that apply to any Registrar to registrar.ts
lightwalker-eth Aug 2, 2024
8515d4a
Significant refactoring
lightwalker-eth Aug 5, 2024
fc782d5
Update packages/ens-utils/src/domain.ts
FrancoAguzzi Aug 6, 2024
08d52b3
Update packages/ens-utils/src/ensname.ts
FrancoAguzzi Aug 6, 2024
00e5d09
Update packages/ens-utils/src/registrar.ts
FrancoAguzzi Aug 6, 2024
909f0cd
Update packages/ens-utils/src/namekitregistrar.ts
FrancoAguzzi Aug 6, 2024
b62ee82
feat: update registrar.ts docs
FrancoAguzzi Aug 6, 2024
d582ba9
feat: create registrar.ts unit tests
FrancoAguzzi Aug 6, 2024
c8440ad
feat: update ethregistrar.ts docs
FrancoAguzzi Aug 6, 2024
cf72935
Merge branch 'refine-domain-logic' of https://github.com/namehash/nam…
FrancoAguzzi Aug 6, 2024
012daf4
Merge remote-tracking branch 'origin/main' into refine-domain-logic
FrancoAguzzi Aug 7, 2024
231a82a
Remove comments that aren't necessarily correct
lightwalker-eth Aug 7, 2024
ae49965
Multiple refinements
lightwalker-eth Aug 7, 2024
442b24a
fix: typo in unit test description
FrancoAguzzi Aug 8, 2024
1192de5
Refine min renewal duration
lightwalker-eth Aug 13, 2024
6c13884
Refine time related logic
lightwalker-eth Aug 13, 2024
1f61716
move scaleAnnualPrice
lightwalker-eth Aug 13, 2024
dc69ace
Refactor Registry
lightwalker-eth Aug 13, 2024
1c2145c
Enhance Registrar
lightwalker-eth Aug 13, 2024
5fb1d09
Refine use of NFTIssuer
lightwalker-eth Aug 13, 2024
54ab2d3
Fix error
lightwalker-eth Aug 13, 2024
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
2 changes: 1 addition & 1 deletion .changeset/neat-lamps-sell.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"@namehash/ens-utils": minor
---

Add domain related logics to ens-utils (Registration, DomainName, DomainCard, etc.)
Add domain related logics to ens-utils (Registration, DomainCard, etc.)
145 changes: 44 additions & 101 deletions packages/ens-utils/src/domain.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { NFTRef } from "./nft";
import { ENSName } from "./ensname";
import { Address, isAddressEqual } from "./address";
import { keccak256, labelhash as labelHash } from "viem";
import { Registration } from "./ethregistrar";
import { Registration } from "./registrar";

export interface DomainCard {
name: ENSName;
Expand All @@ -25,127 +24,71 @@ export interface DomainCard {
formerManagerAddress: Address | null;
}

/* Defines the ownership of a domain for a given address */
/*
* Defines the ownership relation between a domain and a user.
*/
export const UserOwnershipOfDomain = {
/* NoOwner: If domain has no owner */
/*
* The domain has no `ActiveOwner` or `FormerOwner`.
*/
NoOwner: "NoOwner",

/* NotOwner: If domain has an owner but user is not the owner */
/*
* The domain has an `ActiveOwner` or a `FormerOwner` but they are not the
* user.
*/
NotOwner: "NotOwner",

/* FormerOwner: If user is owner of the domain and domain is in Grace Period */
/*
* The user was previously the `ActiveOwner` of the domain, however the
* registration of the domain is now in grace period.
*/
FormerOwner: "FormerOwner",

/* ActiveOwner: If user is owner of the domain and domain is not in Grace Period */
/*
* The user is the owner of the domain that has an active registration (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
* Returns the `UserOwnershipOfDomain` relation between a `DomainCard` and the
* `Address` of the current user.
*
* @param domain The `DomainCard` to check the `UserOwnershipOfDomain`
* relationship with.
* @param currentUserAddress `Address` of the current user, or `null` if there
* is no current user signed in.
* @returns The appropriate `UserOwnershipOfDomain` value given the provided
* `domain` and `currentUserAddress`.
*/
export const getCurrentUserOwnership = (
domain: DomainCard | null,
domain: DomainCard,
currentUserAddress: Address | null,
): 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 (!domain.ownerAddress && !domain.formerOwnerAddress) {
return UserOwnershipOfDomain.NoOwner;
}

if (isOwner) {
if (currentUserAddress) {
if (
domain.ownerAddress &&
isAddressEqual(domain.ownerAddress, currentUserAddress)
) {
return UserOwnershipOfDomain.ActiveOwner;
}
}

if (!ownerAddress) {
return UserOwnershipOfDomain.NoOwner;
if (
domain.formerOwnerAddress &&
isAddressEqual(domain.formerOwnerAddress, currentUserAddress)
) {
return UserOwnershipOfDomain.FormerOwner;
}
}

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;
};
82 changes: 66 additions & 16 deletions packages/ens-utils/src/ensname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
labelhash,
normalizeEncodedLabelhash,
} from "./hashutils";
import { namehash } from "viem";
import { keccak256, namehash } from "viem";

export const LABEL_SEPARATOR = ".";
export const ETH_TLD = "eth";
Expand Down Expand Up @@ -83,21 +83,6 @@ 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 @@ -358,3 +343,68 @@ export function getRegistrationPotential(name: ENSName): RegistrationPotential {
export function charCount(label: string) {
return [...label].length;
}

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

export 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_TLD;

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

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

export 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
};

export 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;
};
76 changes: 2 additions & 74 deletions packages/ens-utils/src/ethregistrar.test.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,7 @@
import { describe, it, expect } from "vitest";
import {
Registrar,
UNWRAPPED_MAINNET_ETH_REGISTRAR,
WRAPPED_MAINNET_ETH_REGISTRAR,
buildNFTRefFromENSName,
} from "./ethregistrar";
import { ENSName, buildENSName } from "./ensname";
import { MAINNET, SEPOLIA } from "./chain";
import { buildNFTRef } from "./nft";
import { describe, it } from "vitest";

// TODO: add a lot more unit tests here

function testNFTRefFromRegistrar(
name: ENSName,
registrar: Registrar,
isWrapped: boolean,
): void {
const expectedToken = registrar.getTokenId(name, isWrapped);
const expectedNFT = buildNFTRef(registrar.contract, expectedToken);
const result = buildNFTRefFromENSName(
name,
registrar.contract.chain,
isWrapped,
);
expect(result).toStrictEqual(expectedNFT);
}

describe("buildNFTRefFromENSName", () => {
it("unrecognized registrar", () => {
expect(() =>
buildNFTRefFromENSName(buildENSName("foo.eth"), SEPOLIA, false),
).toThrow();
});

it("unwrapped non-.eth TLD", () => {
expect(() =>
buildNFTRefFromENSName(buildENSName("foo.com"), MAINNET, false),
).toThrow();
});

it("wrapped non-.eth TLD", () => {
const name = buildENSName("foo.com");
const registrar = WRAPPED_MAINNET_ETH_REGISTRAR;
const isWrapped = true;
testNFTRefFromRegistrar(name, registrar, isWrapped);
});

it("unwrapped subname of a .eth subname", () => {
expect(() =>
buildNFTRefFromENSName(buildENSName("x.foo.eth"), MAINNET, false),
).toThrow();
});

it("wrapped subname of a .eth subname", () => {
const name = buildENSName("x.foo.eth");
const registrar = WRAPPED_MAINNET_ETH_REGISTRAR;
const isWrapped = true;
testNFTRefFromRegistrar(name, registrar, isWrapped);
});

it("unwrapped direct subname of .eth", () => {
const name = buildENSName("foo.eth");
const registrar = UNWRAPPED_MAINNET_ETH_REGISTRAR;
const isWrapped = false;
testNFTRefFromRegistrar(name, registrar, isWrapped);
});

it("wrapped direct subname of .eth", () => {
const name = buildENSName("foo.eth");
const registrar = WRAPPED_MAINNET_ETH_REGISTRAR;
const isWrapped = true;
testNFTRefFromRegistrar(name, registrar, isWrapped);
});
});

describe("getPriceDescription", () => {
/*
The getPriceDescription returns either PriceDescription | null.
Expand All @@ -84,7 +12,7 @@ describe("getPriceDescription", () => {
- pricePerYearDescription is a string that represents: Price + "/ year" (e.g. "$5.99 / year").

In order to return a PriceDescription object, the getPriceDescription function
makes usage of premiumEndsIn function and DOMAIN_HAS_SPECIAL_PRICE_IF_LENGTH_EQUAL_OR_LESS_THAN
makes usage of premiumEndsIn function and DOMAIN_HAS_SPECIAL_PRICE_IF_LENGTH_LESS_THAN
constant, defining by condition the descriptiveTextBeginning, pricePerYear and descriptiveTextEnd.

For every PriceDescription response, the domain price is get from AvailableNameTimelessPriceUSD.
Expand Down
Loading