Skip to content

Commit

Permalink
[TOOL-3009] Dashboard: Support all valid domains as ENS name and not …
Browse files Browse the repository at this point in the history
…just .eth
  • Loading branch information
MananTank committed Jan 13, 2025
1 parent a693958 commit cca9748
Show file tree
Hide file tree
Showing 19 changed files with 173 additions and 49 deletions.
13 changes: 13 additions & 0 deletions .changeset/happy-carrots-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"thirdweb": patch
---

Add `isValidENSName` utility function for checking if a string is a valid ENS name. It does not check if the name is actually registered, it only checks if the string is in a valid format.

```ts
import { isValidENSName } from "thirdweb/utils";

isValidENSName("thirdweb.eth"); // true
isValidENSName("foo.bar.com"); // true
isValidENSName("foo"); // false
```
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { getBytecode, getContract } from "thirdweb/contract";
import { getPublishedUriFromCompilerUri } from "thirdweb/extensions/thirdweb";
import { getInstalledModules } from "thirdweb/modules";
import { download } from "thirdweb/storage";
import { extractIPFSUri } from "thirdweb/utils";
import { extractIPFSUri, isValidENSName } from "thirdweb/utils";
import { fetchPublishedContractsFromDeploy } from "../../../../../../../components/contract-components/fetchPublishedContractsFromDeploy";
import { isEnsName, resolveEns } from "../../../../../../../lib/ens";
import { resolveEns } from "../../../../../../../lib/ens";

type ModuleMetadataPickedKeys = {
publisher: string;
Expand Down Expand Up @@ -52,7 +52,7 @@ export async function getPublishedByCardProps(params: {

// get publisher address/ens
let publisherAddressOrEns = publishedContractToShow.publisher;
if (!isEnsName(publishedContractToShow.publisher)) {
if (!isValidENSName(publishedContractToShow.publisher)) {
try {
const res = await resolveEns(publishedContractToShow.publisher);
if (res.ensName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getAddress, isAddress } from "thirdweb";
import { isValidENSName } from "thirdweb/utils";
import { mapThirdwebPublisher } from "../../../../components/contract-components/fetch-contracts-with-versions";
import { isEnsName, resolveEns } from "../../../../lib/ens";
import { resolveEns } from "../../../../lib/ens";

type ResolvedAddressInfo = {
address: string;
Expand All @@ -17,7 +18,7 @@ export async function resolveAddressAndEns(
};
}

if (isEnsName(addressOrEns)) {
if (isValidENSName(addressOrEns)) {
const mappedEns = mapThirdwebPublisher(addressOrEns);
const res = await resolveEns(mappedEns).catch(() => null);
if (res?.address) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export default async function PublishedContractPage(
<SimpleGrid columns={12} gap={{ base: 6, md: 10 }} w="full">
<PublishedContract
publishedContract={publishedContract}
walletOrEns={params.publisher}
twAccount={account}
/>
</SimpleGrid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export default async function PublishedContractPage(
<div className="grid w-full grid-cols-12 gap-6 md:gap-10">
<PublishedContract
publishedContract={publishedContract}
walletOrEns={params.publisher}
twAccount={account}
/>
</div>
Expand Down
10 changes: 5 additions & 5 deletions apps/dashboard/src/components/contract-components/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { queryOptions, useQuery } from "@tanstack/react-query";
import type { Abi } from "abitype";
import { isEnsName, resolveEns } from "lib/ens";
import { resolveEns } from "lib/ens";
import { useV5DashboardChain } from "lib/v5-adapter";
import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
import { getContract, resolveContractAbi } from "thirdweb/contract";
import { isAddress } from "thirdweb/utils";
import { isAddress, isValidENSName } from "thirdweb/utils";
import {
type PublishedContractWithVersion,
fetchPublishedContractVersions,
Expand Down Expand Up @@ -130,7 +130,7 @@ function ensQuery(addressOrEnsName?: string) {
return placeholderData;
}
// if it is neither an address or an ens name then return the placeholder data only
if (!isAddress(addressOrEnsName) && !isEnsName(addressOrEnsName)) {
if (!isAddress(addressOrEnsName) && !isValidENSName(addressOrEnsName)) {
throw new Error("Invalid address or ENS name.");
}

Expand All @@ -143,7 +143,7 @@ function ensQuery(addressOrEnsName?: string) {
}),
);

if (isEnsName(addressOrEnsName) && !address) {
if (isValidENSName(addressOrEnsName) && !address) {
throw new Error("Failed to resolve ENS name.");
}

Expand All @@ -154,7 +154,7 @@ function ensQuery(addressOrEnsName?: string) {
},
enabled:
!!addressOrEnsName &&
(isAddress(addressOrEnsName) || isEnsName(addressOrEnsName)),
(isAddress(addressOrEnsName) || isValidENSName(addressOrEnsName)),
// 24h
gcTime: 60 * 60 * 24 * 1000,
// 1h
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,11 @@ interface ExtendedPublishedContract extends PublishedContractWithVersion {

interface PublishedContractProps {
publishedContract: ExtendedPublishedContract;
walletOrEns: string;
twAccount: Account | undefined;
}

export const PublishedContract: React.FC<PublishedContractProps> = ({
publishedContract,
walletOrEns,
twAccount,
}) => {
const address = useActiveAccount()?.address;
Expand Down Expand Up @@ -154,7 +152,9 @@ export const PublishedContract: React.FC<PublishedContractProps> = ({
</GridItem>
<GridItem colSpan={{ base: 12, md: 3 }}>
<Flex flexDir="column" gap={6}>
{walletOrEns && <PublisherHeader wallet={walletOrEns} />}
{publishedContract.publisher && (
<PublisherHeader wallet={publishedContract.publisher} />
)}
<Divider />
<Flex flexDir="column" gap={4}>
<Heading as="h4" size="title.sm">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,16 @@ export const PublisherHeader: React.FC<PublisherHeaderProps> = ({
>
<AccountName
fallbackComponent={
<AccountAddress
formatFn={(addr) =>
shortenIfAddress(replaceDeployerAddress(addr))
}
/>
// When social profile API support other TLDs as well - we can remove this condition
ensQuery.data?.ensName ? (
<span> {ensQuery.data?.ensName} </span>
) : (
<AccountAddress
formatFn={(addr) =>
shortenIfAddress(replaceDeployerAddress(addr))
}
/>
)
}
loadingComponent={<Skeleton className="h-8 w-40" />}
formatFn={(name) => replaceDeployerAddress(name)}
Expand Down
17 changes: 5 additions & 12 deletions apps/dashboard/src/constants/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { resolveEns } from "lib/ens";
import { isAddress } from "thirdweb";
import { isValidENSName } from "thirdweb/utils";
import z from "zod";

/**
Expand All @@ -15,19 +16,11 @@ export const BasisPointsSchema = z
.min(0, "Cannot be below 0%");

// @internal
type EnsName = `${string}.eth` | `${string}.cb.id`;
type EnsName = string;

// Only pass through to provider call if value ends with .eth or .cb.id
const EnsSchema: z.ZodType<
`0x${string}`,
z.ZodTypeDef,
`${string}.eth` | `${string}.cb.id`
> = z
.custom<EnsName>(
(ens) =>
typeof ens === "string" &&
(ens.endsWith(".eth") || ens.endsWith(".cb.id")),
)
// Only pass through to provider call if value is a valid ENS name
const EnsSchema: z.ZodType<`0x${string}`, z.ZodTypeDef, string> = z
.custom<EnsName>((ens) => typeof ens === "string" && isValidENSName(ens))
.transform(async (ens) => (await resolveEns(ens)).address)
.refine(
(address): address is `0x${string}` => !!address && isAddress(address),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useEns } from "components/contract-components/hooks";
import { CheckIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useActiveAccount } from "thirdweb/react";
import { isAddress } from "thirdweb/utils";
import { isAddress, isValidENSName } from "thirdweb/utils";
import { FormHelperText } from "tw-components";
import type { SolidityInputProps } from ".";
import { validateAddress } from "./helpers";
Expand Down Expand Up @@ -77,14 +77,19 @@ export const SolidityAddressInput: React.FC<SolidityInputProps> = ({

const resolvingEns = useMemo(
() =>
localInput?.endsWith(".eth") &&
localInput &&
isValidENSName(localInput) &&
!ensQuery.isError &&
!ensQuery.data?.address,
[ensQuery.data?.address, ensQuery.isError, localInput],
);

const resolvedAddress = useMemo(
() => localInput?.endsWith(".eth") && !hasError && ensQuery.data?.address,
() =>
localInput &&
isValidENSName(localInput) &&
!hasError &&
ensQuery.data?.address,
[ensQuery.data?.address, hasError, localInput],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isAddress, isBytes, isHex } from "thirdweb/utils";
import { isAddress, isBytes, isHex, isValidENSName } from "thirdweb/utils";

// int and uint
function calculateIntMinValues(solidityType: string) {
Expand Down Expand Up @@ -147,7 +147,7 @@ export const validateBytes = (value: string, solidityType: string) => {

// address
export const validateAddress = (value: string) => {
if (!isAddress(value) && !value.endsWith(".eth")) {
if (!isAddress(value) && !isValidENSName(value)) {
return {
type: "pattern",
message: "Input is not a valid address or ENS name.",
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/lib/address-utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { isAddress } from "thirdweb";
import { isEnsName } from "./ens";
import { isValidENSName } from "thirdweb/utils";

// if a string is a valid address or ens name
export function isPossibleEVMAddress(address?: string, ignoreEns?: boolean) {
if (!address) {
return false;
}
if (isEnsName(address) && !ignoreEns) {
if (isValidENSName(address) && !ignoreEns) {
return true;
}
return isAddress(address);
Expand Down
7 changes: 2 additions & 5 deletions apps/dashboard/src/lib/ens.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { isAddress } from "thirdweb";
import { resolveAddress, resolveName } from "thirdweb/extensions/ens";
import { isValidENSName } from "thirdweb/utils";

interface ENSResolveResult {
ensName: string | null;
address: string | null;
}

export function isEnsName(name: string): boolean {
return name?.endsWith(".eth");
}

export async function resolveEns(
ensNameOrAddress: string,
): Promise<ENSResolveResult> {
Expand All @@ -24,7 +21,7 @@ export async function resolveEns(
};
}

if (!isEnsName(ensNameOrAddress)) {
if (!isValidENSName(ensNameOrAddress)) {
throw new Error("Invalid ENS name");
}

Expand Down
7 changes: 4 additions & 3 deletions apps/dashboard/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { type NextRequest, NextResponse } from "next/server";
import { getAddress } from "thirdweb";
import { getChainMetadata } from "thirdweb/chains";
import { isValidENSName } from "thirdweb/utils";
import { defineDashboardChain } from "./lib/defineDashboardChain";

// ignore assets, api - only intercept page routes
Expand Down Expand Up @@ -136,7 +137,7 @@ export async function middleware(request: NextRequest) {
// DIFFERENT DYNAMIC ROUTING CASES

// /<address>/... case
if (paths[0] && isPossibleEVMAddress(paths[0])) {
if (paths[0] && isPossibleAddressOrENSName(paths[0])) {
// special case for "deployer.thirdweb.eth"
// we want to always redirect this to "thirdweb.eth/..."
if (paths[0] === "deployer.thirdweb.eth") {
Expand Down Expand Up @@ -181,8 +182,8 @@ export async function middleware(request: NextRequest) {
}
}

function isPossibleEVMAddress(address: string) {
return address?.startsWith("0x") || address?.endsWith(".eth");
function isPossibleAddressOrENSName(address: string) {
return address.startsWith("0x") || isValidENSName(address);
}

// utils for rewriting and redirecting with relative paths
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { isAddress } from "thirdweb";
import { resolveAddress } from "thirdweb/extensions/ens";
import { type SocialProfile, getSocialProfiles } from "thirdweb/social";
import { resolveScheme } from "thirdweb/storage";
import { isValidENSName } from "thirdweb/utils";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
Expand All @@ -19,7 +20,7 @@ export function SocialProfiles() {
const { mutate: searchProfiles, isPending } = useMutation({
mutationFn: async (address: string) => {
const resolvedAddress = await (async () => {
if (address.endsWith(".eth")) {
if (isValidENSName(address)) {
return resolveAddress({
client: THIRDWEB_CLIENT,
name: address,
Expand Down
3 changes: 3 additions & 0 deletions packages/thirdweb/src/exports/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,6 @@ export type {

export { shortenLargeNumber } from "../utils/shortenLargeNumber.js";
export { formatNumber } from "../utils/formatNumber.js";

// ENS
export { isValidENSName } from "../utils/ens/isValidENSName.js";
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { transfer } from "../../../../extensions/erc20/write/transfer.js";
import { sendTransaction } from "../../../../transaction/actions/send-transaction.js";
import { prepareTransaction } from "../../../../transaction/prepare-transaction.js";
import { isAddress } from "../../../../utils/address.js";
import { isValidENSName } from "../../../../utils/ens/isValidENSName.js";
import { toWei } from "../../../../utils/units.js";
import { useActiveWallet } from "./useActiveWallet.js";

Expand Down Expand Up @@ -53,7 +54,7 @@ export function useSendToken(client: ThirdwebClient) {
// input validation
if (
!receiverAddress ||
(!receiverAddress.endsWith(".eth") && !isAddress(receiverAddress))
(!isValidENSName(receiverAddress) && !isAddress(receiverAddress))
) {
throw new Error("Invalid receiver address");
}
Expand Down
44 changes: 44 additions & 0 deletions packages/thirdweb/src/utils/ens/isValidENSName.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { isValidENSName } from "./isValidENSName.js";

describe("isValidENSName", () => {
it("should return true for a valid ENS name", () => {
expect(isValidENSName("thirdweb.eth")).toBe(true);
expect(isValidENSName("deployer.thirdweb.eth")).toBe(true);
expect(isValidENSName("x.eth")).toBe(true);
expect(isValidENSName("foo.bar.com")).toBe(true);
expect(isValidENSName("foo.com")).toBe(true);
expect(isValidENSName("somename.xyz")).toBe(true);
});

it("should return false for an invalid ENS name", () => {
// No TLD
expect(isValidENSName("")).toBe(false);
expect(isValidENSName("foo")).toBe(false);

// parts with length < 2
expect(isValidENSName(".eth")).toBe(false);
expect(isValidENSName("thirdweb.eth.")).toBe(false);

// numeric TLD
expect(isValidENSName("foo.123")).toBe(false);

// whitespace in parts
expect(isValidENSName("foo .com")).toBe(false);
expect(isValidENSName("foo. com")).toBe(false);

// full-width characters
expect(isValidENSName("foo.bar.com")).toBe(false);

// wildcard characters
expect(isValidENSName("foo*bar.com")).toBe(false);

// starts with a hyphen
expect(isValidENSName("-foo.bar.com")).toBe(false);
// ends with a hyphen
expect(isValidENSName("foo.bar.com-")).toBe(false);

// underscore
expect(isValidENSName("foo_bar.com")).toBe(false);
});
});
Loading

0 comments on commit cca9748

Please sign in to comment.