Skip to content

Commit 0e2b3df

Browse files
committed
[TOOL-3009] Dashboard: Support all valid domains as ENS name and not just .eth (#5937)
<!-- start pr-codex --> ## PR-Codex overview This PR introduces the `isValidENSName` utility function to validate ENS names, replacing the previous `isEnsName` checks across multiple files. It refines address validation and enhances the overall handling of ENS names within the codebase. ### Detailed summary - Added `isValidENSName` utility function for ENS name validation. - Replaced occurrences of `isEnsName` with `isValidENSName` in various files. - Updated validation logic in components and utilities to improve address and ENS name handling. - Enhanced error messages and conditions related to ENS name checks. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent c339803 commit 0e2b3df

File tree

19 files changed

+160
-49
lines changed

19 files changed

+160
-49
lines changed

.changeset/happy-carrots-appear.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
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.
6+
7+
```ts
8+
import { isValidENSName } from "thirdweb/utils";
9+
10+
isValidENSName("thirdweb.eth"); // true
11+
isValidENSName("foo.bar.com"); // true
12+
isValidENSName("foo"); // false
13+
```

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/published-by-ui.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { getBytecode, getContract } from "thirdweb/contract";
66
import { getPublishedUriFromCompilerUri } from "thirdweb/extensions/thirdweb";
77
import { getInstalledModules } from "thirdweb/modules";
88
import { download } from "thirdweb/storage";
9-
import { extractIPFSUri } from "thirdweb/utils";
9+
import { extractIPFSUri, isValidENSName } from "thirdweb/utils";
1010
import { fetchPublishedContractsFromDeploy } from "../../../../../../../components/contract-components/fetchPublishedContractsFromDeploy";
11-
import { isEnsName, resolveEns } from "../../../../../../../lib/ens";
11+
import { resolveEns } from "../../../../../../../lib/ens";
1212

1313
type ModuleMetadataPickedKeys = {
1414
publisher: string;
@@ -52,7 +52,7 @@ export async function getPublishedByCardProps(params: {
5252

5353
// get publisher address/ens
5454
let publisherAddressOrEns = publishedContractToShow.publisher;
55-
if (!isEnsName(publishedContractToShow.publisher)) {
55+
if (!isValidENSName(publishedContractToShow.publisher)) {
5656
try {
5757
const res = await resolveEns(publishedContractToShow.publisher);
5858
if (res.ensName) {

apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/resolveAddressAndEns.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getAddress, isAddress } from "thirdweb";
2+
import { isValidENSName } from "thirdweb/utils";
23
import { mapThirdwebPublisher } from "../../../../components/contract-components/fetch-contracts-with-versions";
3-
import { isEnsName, resolveEns } from "../../../../lib/ens";
4+
import { resolveEns } from "../../../../lib/ens";
45

56
type ResolvedAddressInfo = {
67
address: string;
@@ -17,7 +18,7 @@ export async function resolveAddressAndEns(
1718
};
1819
}
1920

20-
if (isEnsName(addressOrEns)) {
21+
if (isValidENSName(addressOrEns)) {
2122
const mappedEns = mapThirdwebPublisher(addressOrEns);
2223
const res = await resolveEns(mappedEns).catch(() => null);
2324
if (res?.address) {

apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/[version]/page.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ export default async function PublishedContractPage(
8585
<SimpleGrid columns={12} gap={{ base: 6, md: 10 }} w="full">
8686
<PublishedContract
8787
publishedContract={publishedContract}
88-
walletOrEns={params.publisher}
8988
twAccount={account}
9089
/>
9190
</SimpleGrid>

apps/dashboard/src/app/(dashboard)/published-contract/[publisher]/[contract_id]/page.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ export default async function PublishedContractPage(
5454
<div className="grid w-full grid-cols-12 gap-6 md:gap-10">
5555
<PublishedContract
5656
publishedContract={publishedContract}
57-
walletOrEns={params.publisher}
5857
twAccount={account}
5958
/>
6059
</div>

apps/dashboard/src/components/contract-components/hooks.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import { useThirdwebClient } from "@/constants/thirdweb.client";
44
import { queryOptions, useQuery } from "@tanstack/react-query";
55
import type { Abi } from "abitype";
6-
import { isEnsName, resolveEns } from "lib/ens";
6+
import { resolveEns } from "lib/ens";
77
import { useV5DashboardChain } from "lib/v5-adapter";
88
import { useMemo } from "react";
99
import type { ThirdwebContract } from "thirdweb";
1010
import { getContract, resolveContractAbi } from "thirdweb/contract";
11-
import { isAddress } from "thirdweb/utils";
11+
import { isAddress, isValidENSName } from "thirdweb/utils";
1212
import {
1313
type PublishedContractWithVersion,
1414
fetchPublishedContractVersions,
@@ -130,7 +130,7 @@ function ensQuery(addressOrEnsName?: string) {
130130
return placeholderData;
131131
}
132132
// if it is neither an address or an ens name then return the placeholder data only
133-
if (!isAddress(addressOrEnsName) && !isEnsName(addressOrEnsName)) {
133+
if (!isAddress(addressOrEnsName) && !isValidENSName(addressOrEnsName)) {
134134
throw new Error("Invalid address or ENS name.");
135135
}
136136

@@ -143,7 +143,7 @@ function ensQuery(addressOrEnsName?: string) {
143143
}),
144144
);
145145

146-
if (isEnsName(addressOrEnsName) && !address) {
146+
if (isValidENSName(addressOrEnsName) && !address) {
147147
throw new Error("Failed to resolve ENS name.");
148148
}
149149

@@ -154,7 +154,7 @@ function ensQuery(addressOrEnsName?: string) {
154154
},
155155
enabled:
156156
!!addressOrEnsName &&
157-
(isAddress(addressOrEnsName) || isEnsName(addressOrEnsName)),
157+
(isAddress(addressOrEnsName) || isValidENSName(addressOrEnsName)),
158158
// 24h
159159
gcTime: 60 * 60 * 24 * 1000,
160160
// 1h

apps/dashboard/src/components/contract-components/published-contract/index.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,11 @@ interface ExtendedPublishedContract extends PublishedContractWithVersion {
4242

4343
interface PublishedContractProps {
4444
publishedContract: ExtendedPublishedContract;
45-
walletOrEns: string;
4645
twAccount: Account | undefined;
4746
}
4847

4948
export const PublishedContract: React.FC<PublishedContractProps> = ({
5049
publishedContract,
51-
walletOrEns,
5250
twAccount,
5351
}) => {
5452
const address = useActiveAccount()?.address;
@@ -154,7 +152,9 @@ export const PublishedContract: React.FC<PublishedContractProps> = ({
154152
</GridItem>
155153
<GridItem colSpan={{ base: 12, md: 3 }}>
156154
<Flex flexDir="column" gap={6}>
157-
{walletOrEns && <PublisherHeader wallet={walletOrEns} />}
155+
{publishedContract.publisher && (
156+
<PublisherHeader wallet={publishedContract.publisher} />
157+
)}
158158
<Divider />
159159
<Flex flexDir="column" gap={4}>
160160
<Heading as="h4" size="title.sm">

apps/dashboard/src/components/contract-components/publisher/publisher-header.tsx

+10-5
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,16 @@ export const PublisherHeader: React.FC<PublisherHeaderProps> = ({
8383
>
8484
<AccountName
8585
fallbackComponent={
86-
<AccountAddress
87-
formatFn={(addr) =>
88-
shortenIfAddress(replaceDeployerAddress(addr))
89-
}
90-
/>
86+
// When social profile API support other TLDs as well - we can remove this condition
87+
ensQuery.data?.ensName ? (
88+
<span> {ensQuery.data?.ensName} </span>
89+
) : (
90+
<AccountAddress
91+
formatFn={(addr) =>
92+
shortenIfAddress(replaceDeployerAddress(addr))
93+
}
94+
/>
95+
)
9196
}
9297
loadingComponent={<Skeleton className="h-8 w-40" />}
9398
formatFn={(name) => replaceDeployerAddress(name)}

apps/dashboard/src/constants/schemas.ts

+5-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { resolveEns } from "lib/ens";
22
import { isAddress } from "thirdweb";
3+
import { isValidENSName } from "thirdweb/utils";
34
import z from "zod";
45

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

1718
// @internal
18-
type EnsName = `${string}.eth` | `${string}.cb.id`;
19+
type EnsName = string;
1920

20-
// Only pass through to provider call if value ends with .eth or .cb.id
21-
const EnsSchema: z.ZodType<
22-
`0x${string}`,
23-
z.ZodTypeDef,
24-
`${string}.eth` | `${string}.cb.id`
25-
> = z
26-
.custom<EnsName>(
27-
(ens) =>
28-
typeof ens === "string" &&
29-
(ens.endsWith(".eth") || ens.endsWith(".cb.id")),
30-
)
21+
// Only pass through to provider call if value is a valid ENS name
22+
const EnsSchema: z.ZodType<`0x${string}`, z.ZodTypeDef, string> = z
23+
.custom<EnsName>((ens) => typeof ens === "string" && isValidENSName(ens))
3124
.transform(async (ens) => (await resolveEns(ens)).address)
3225
.refine(
3326
(address): address is `0x${string}` => !!address && isAddress(address),

apps/dashboard/src/contract-ui/components/solidity-inputs/address-input.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useEns } from "components/contract-components/hooks";
55
import { CheckIcon } from "lucide-react";
66
import { useEffect, useMemo, useState } from "react";
77
import { useActiveAccount } from "thirdweb/react";
8-
import { isAddress } from "thirdweb/utils";
8+
import { isAddress, isValidENSName } from "thirdweb/utils";
99
import { FormHelperText } from "tw-components";
1010
import type { SolidityInputProps } from ".";
1111
import { validateAddress } from "./helpers";
@@ -77,14 +77,19 @@ export const SolidityAddressInput: React.FC<SolidityInputProps> = ({
7777

7878
const resolvingEns = useMemo(
7979
() =>
80-
localInput?.endsWith(".eth") &&
80+
localInput &&
81+
isValidENSName(localInput) &&
8182
!ensQuery.isError &&
8283
!ensQuery.data?.address,
8384
[ensQuery.data?.address, ensQuery.isError, localInput],
8485
);
8586

8687
const resolvedAddress = useMemo(
87-
() => localInput?.endsWith(".eth") && !hasError && ensQuery.data?.address,
88+
() =>
89+
localInput &&
90+
isValidENSName(localInput) &&
91+
!hasError &&
92+
ensQuery.data?.address,
8893
[ensQuery.data?.address, hasError, localInput],
8994
);
9095

apps/dashboard/src/contract-ui/components/solidity-inputs/helpers.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isAddress, isBytes, isHex } from "thirdweb/utils";
1+
import { isAddress, isBytes, isHex, isValidENSName } from "thirdweb/utils";
22

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

148148
// address
149149
export const validateAddress = (value: string) => {
150-
if (!isAddress(value) && !value.endsWith(".eth")) {
150+
if (!isAddress(value) && !isValidENSName(value)) {
151151
return {
152152
type: "pattern",
153153
message: "Input is not a valid address or ENS name.",

apps/dashboard/src/lib/address-utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { isAddress } from "thirdweb";
2-
import { isEnsName } from "./ens";
2+
import { isValidENSName } from "thirdweb/utils";
33

44
// if a string is a valid address or ens name
55
export function isPossibleEVMAddress(address?: string, ignoreEns?: boolean) {
66
if (!address) {
77
return false;
88
}
9-
if (isEnsName(address) && !ignoreEns) {
9+
if (isValidENSName(address) && !ignoreEns) {
1010
return true;
1111
}
1212
return isAddress(address);

apps/dashboard/src/lib/ens.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import { getThirdwebClient } from "@/constants/thirdweb.server";
22
import { isAddress } from "thirdweb";
33
import { resolveAddress, resolveName } from "thirdweb/extensions/ens";
4+
import { isValidENSName } from "thirdweb/utils";
45

56
interface ENSResolveResult {
67
ensName: string | null;
78
address: string | null;
89
}
910

10-
export function isEnsName(name: string): boolean {
11-
return name?.endsWith(".eth");
12-
}
13-
1411
export async function resolveEns(
1512
ensNameOrAddress: string,
1613
): Promise<ENSResolveResult> {
@@ -24,7 +21,7 @@ export async function resolveEns(
2421
};
2522
}
2623

27-
if (!isEnsName(ensNameOrAddress)) {
24+
if (!isValidENSName(ensNameOrAddress)) {
2825
throw new Error("Invalid ENS name");
2926
}
3027

apps/dashboard/src/middleware.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
44
import { type NextRequest, NextResponse } from "next/server";
55
import { getAddress } from "thirdweb";
66
import { getChainMetadata } from "thirdweb/chains";
7+
import { isValidENSName } from "thirdweb/utils";
78
import { defineDashboardChain } from "./lib/defineDashboardChain";
89

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

138139
// /<address>/... case
139-
if (paths[0] && isPossibleEVMAddress(paths[0])) {
140+
if (paths[0] && isPossibleAddressOrENSName(paths[0])) {
140141
// special case for "deployer.thirdweb.eth"
141142
// we want to always redirect this to "thirdweb.eth/..."
142143
if (paths[0] === "deployer.thirdweb.eth") {
@@ -181,8 +182,8 @@ export async function middleware(request: NextRequest) {
181182
}
182183
}
183184

184-
function isPossibleEVMAddress(address: string) {
185-
return address?.startsWith("0x") || address?.endsWith(".eth");
185+
function isPossibleAddressOrENSName(address: string) {
186+
return address.startsWith("0x") || isValidENSName(address);
186187
}
187188

188189
// utils for rewriting and redirecting with relative paths

apps/playground-web/src/components/social/social-profiles.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { isAddress } from "thirdweb";
88
import { resolveAddress } from "thirdweb/extensions/ens";
99
import { type SocialProfile, getSocialProfiles } from "thirdweb/social";
1010
import { resolveScheme } from "thirdweb/storage";
11+
import { isValidENSName } from "thirdweb/utils";
1112
import { Badge } from "../ui/badge";
1213
import { Button } from "../ui/button";
1314
import { Input } from "../ui/input";
@@ -19,7 +20,7 @@ export function SocialProfiles() {
1920
const { mutate: searchProfiles, isPending } = useMutation({
2021
mutationFn: async (address: string) => {
2122
const resolvedAddress = await (async () => {
22-
if (address.endsWith(".eth")) {
23+
if (isValidENSName(address)) {
2324
return resolveAddress({
2425
client: THIRDWEB_CLIENT,
2526
name: address,

packages/thirdweb/src/exports/utils.ts

+3
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,6 @@ export type {
208208

209209
export { shortenLargeNumber } from "../utils/shortenLargeNumber.js";
210210
export { formatNumber } from "../utils/formatNumber.js";
211+
212+
// ENS
213+
export { isValidENSName } from "../utils/ens/isValidENSName.js";

packages/thirdweb/src/react/core/hooks/wallets/useSendToken.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { transfer } from "../../../../extensions/erc20/write/transfer.js";
66
import { sendTransaction } from "../../../../transaction/actions/send-transaction.js";
77
import { prepareTransaction } from "../../../../transaction/prepare-transaction.js";
88
import { isAddress } from "../../../../utils/address.js";
9+
import { isValidENSName } from "../../../../utils/ens/isValidENSName.js";
910
import { toWei } from "../../../../utils/units.js";
1011
import { useActiveWallet } from "./useActiveWallet.js";
1112

@@ -53,7 +54,7 @@ export function useSendToken(client: ThirdwebClient) {
5354
// input validation
5455
if (
5556
!receiverAddress ||
56-
(!receiverAddress.endsWith(".eth") && !isAddress(receiverAddress))
57+
(!isValidENSName(receiverAddress) && !isAddress(receiverAddress))
5758
) {
5859
throw new Error("Invalid receiver address");
5960
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, expect, it } from "vitest";
2+
import { isValidENSName } from "./isValidENSName.js";
3+
4+
describe("isValidENSName", () => {
5+
it("should return true for a valid ENS name", () => {
6+
expect(isValidENSName("thirdweb.eth")).toBe(true);
7+
expect(isValidENSName("deployer.thirdweb.eth")).toBe(true);
8+
expect(isValidENSName("x.eth")).toBe(true);
9+
expect(isValidENSName("foo.bar.com")).toBe(true);
10+
expect(isValidENSName("foo.com")).toBe(true);
11+
expect(isValidENSName("somename.xyz")).toBe(true);
12+
expect(isValidENSName("_foo.bar")).toBe(true);
13+
expect(isValidENSName("-foo.bar.com")).toBe(true);
14+
});
15+
16+
it("should return false for an invalid ENS name", () => {
17+
// No TLD
18+
expect(isValidENSName("")).toBe(false);
19+
expect(isValidENSName("foo")).toBe(false);
20+
21+
// parts with length < 2
22+
expect(isValidENSName(".eth")).toBe(false);
23+
expect(isValidENSName("foo..com")).toBe(false);
24+
expect(isValidENSName("thirdweb.eth.")).toBe(false);
25+
26+
// numeric TLD
27+
expect(isValidENSName("foo.123")).toBe(false);
28+
29+
// whitespace in parts
30+
expect(isValidENSName("foo .com")).toBe(false);
31+
expect(isValidENSName("foo. com")).toBe(false);
32+
33+
// full-width characters
34+
expect(isValidENSName("foo.bar.com")).toBe(false);
35+
36+
// wildcard characters
37+
expect(isValidENSName("foo*bar.com")).toBe(false);
38+
});
39+
});

0 commit comments

Comments
 (0)